chore(frontend): introduce prettier + eslint and prune unused code
- Add Prettier 3 with prettier-plugin-organize-imports (sorts/removes unused imports) - Add ESLint 10 flat config with typescript-eslint + eslint-plugin-vue + eslint-config-prettier - Add root scripts: format, format:check, lint, lint:fix - Reformat 257 files across admin-web, ops-web, desktop-client, packages - Remove unused locals/exports flagged by --noUnusedLocals/--noUnusedParameters - Fix duplicate localTabLabel key, surrogate-pair regex u-flag, NBSP literal in regex - Skip server/ (Go) and apps/browser-extension/ (deprecated per ADR) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,145 +1,147 @@
|
||||
import type { JsonValue } from "@geo/shared-types";
|
||||
import type { JsonValue } from '@geo/shared-types'
|
||||
|
||||
import type { PublishAdapter, PublishAdapterContext } from './base'
|
||||
import {
|
||||
extractImageSources,
|
||||
normalizeArticleHtml,
|
||||
normalizeRemoteUrl,
|
||||
sessionCookieHeader,
|
||||
sessionFetchJson,
|
||||
} from "./common";
|
||||
import {
|
||||
cropImageAssetBlob,
|
||||
fetchImageAssetBlob,
|
||||
type ImageAssetBlob,
|
||||
} from "./media-image";
|
||||
import type { PublishAdapter, PublishAdapterContext } from "./base";
|
||||
} from './common'
|
||||
import { cropImageAssetBlob, fetchImageAssetBlob, type ImageAssetBlob } from './media-image'
|
||||
|
||||
type ZolUserInfoResponse = {
|
||||
data?: {
|
||||
userId?: string | number;
|
||||
nickName?: string;
|
||||
photo?: string;
|
||||
};
|
||||
};
|
||||
userId?: string | number
|
||||
nickName?: string
|
||||
photo?: string
|
||||
}
|
||||
}
|
||||
|
||||
type ZolDraftResponse = {
|
||||
errcode?: number;
|
||||
errmsg?: string;
|
||||
errcode?: number
|
||||
errmsg?: string
|
||||
data?: {
|
||||
draftId?: string | number;
|
||||
};
|
||||
};
|
||||
draftId?: string | number
|
||||
}
|
||||
}
|
||||
|
||||
type ZolPublishResponse = {
|
||||
errcode?: number;
|
||||
errmsg?: string;
|
||||
errcode?: number
|
||||
errmsg?: string
|
||||
data?: {
|
||||
contentId?: string | number;
|
||||
};
|
||||
};
|
||||
contentId?: string | number
|
||||
}
|
||||
}
|
||||
|
||||
type ZolImageUploadResponse = {
|
||||
errcode?: number;
|
||||
errmsg?: string;
|
||||
errcode?: number
|
||||
errmsg?: string
|
||||
data?: {
|
||||
fileUrl?: string;
|
||||
pic?: string;
|
||||
};
|
||||
};
|
||||
fileUrl?: string
|
||||
pic?: string
|
||||
}
|
||||
}
|
||||
|
||||
type ZolSubjectListResponse = {
|
||||
data?: {
|
||||
list?: Array<{
|
||||
subjectId?: string | number;
|
||||
subjectName?: string;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
|
||||
type ZolAccount = {
|
||||
uid: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
type ZolCoverImage = {
|
||||
url: string;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
const ZOL_API_ORIGIN = "https://open-api.zol.com.cn";
|
||||
const ZOL_REFERER = "https://post.zol.com.cn";
|
||||
const ZOL_MANAGE_URL = "https://post.zol.com.cn/v2/manage/works/all";
|
||||
const ZOL_SUBJECT_LIMIT = 5;
|
||||
const LOCAL_IMAGE_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "0.0.0.0", "api"]);
|
||||
|
||||
function stringId(value: unknown): string {
|
||||
if (typeof value === "number") {
|
||||
return Number.isFinite(value) ? String(value) : "";
|
||||
subjectId?: string | number
|
||||
subjectName?: string
|
||||
}>
|
||||
}
|
||||
if (typeof value !== "string") {
|
||||
return "";
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
function zolResponseMessage(response: { errmsg?: string; errcode?: number } | null | undefined): string {
|
||||
return response?.errmsg || `zol_error_${response?.errcode ?? "unknown"}`;
|
||||
type ZolAccount = {
|
||||
uid: string
|
||||
name: string
|
||||
}
|
||||
|
||||
type ZolCoverImage = {
|
||||
url: string
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
const ZOL_API_ORIGIN = 'https://open-api.zol.com.cn'
|
||||
const ZOL_REFERER = 'https://post.zol.com.cn'
|
||||
const ZOL_MANAGE_URL = 'https://post.zol.com.cn/v2/manage/works/all'
|
||||
const ZOL_SUBJECT_LIMIT = 5
|
||||
const LOCAL_IMAGE_HOSTS = new Set(['localhost', '127.0.0.1', '::1', '0.0.0.0', 'api'])
|
||||
|
||||
function stringId(value: unknown): string {
|
||||
if (typeof value === 'number') {
|
||||
return Number.isFinite(value) ? String(value) : ''
|
||||
}
|
||||
if (typeof value !== 'string') {
|
||||
return ''
|
||||
}
|
||||
return value.trim()
|
||||
}
|
||||
|
||||
function zolResponseMessage(
|
||||
response: { errmsg?: string; errcode?: number } | null | undefined,
|
||||
): string {
|
||||
return response?.errmsg || `zol_error_${response?.errcode ?? 'unknown'}`
|
||||
}
|
||||
|
||||
function isZolChallengeMessage(message: string): boolean {
|
||||
return /(验证码|滑块|人机验证|安全验证|请完成验证|短信验证|扫码验证|captcha|challenge|verification required|风险|风控)/i
|
||||
.test(message);
|
||||
return /(验证码|滑块|人机验证|安全验证|请完成验证|短信验证|扫码验证|captcha|challenge|verification required|风险|风控)/i.test(
|
||||
message,
|
||||
)
|
||||
}
|
||||
|
||||
export function normalizeZolUploadedImageUrl(value: string | null | undefined): string | null {
|
||||
const trimmed = value?.trim();
|
||||
const trimmed = value?.trim()
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
if (/^\/\//.test(trimmed)) {
|
||||
return `https:${trimmed}`;
|
||||
return `https:${trimmed}`
|
||||
}
|
||||
if (/^\//.test(trimmed)) {
|
||||
return new URL(trimmed, ZOL_REFERER).toString();
|
||||
return new URL(trimmed, ZOL_REFERER).toString()
|
||||
}
|
||||
return trimmed;
|
||||
return trimmed
|
||||
}
|
||||
|
||||
function zolUploadedImageUrl(response: ZolImageUploadResponse | null | undefined): string | null {
|
||||
return normalizeZolUploadedImageUrl(response?.data?.pic) ?? normalizeZolUploadedImageUrl(response?.data?.fileUrl);
|
||||
return (
|
||||
normalizeZolUploadedImageUrl(response?.data?.pic) ??
|
||||
normalizeZolUploadedImageUrl(response?.data?.fileUrl)
|
||||
)
|
||||
}
|
||||
|
||||
function zolImageUploadMessage(response: ZolImageUploadResponse | null | undefined): string {
|
||||
if (!response) {
|
||||
return "empty_response";
|
||||
return 'empty_response'
|
||||
}
|
||||
return zolResponseMessage(response);
|
||||
return zolResponseMessage(response)
|
||||
}
|
||||
|
||||
function isLocalImageHost(hostname: string): boolean {
|
||||
return LOCAL_IMAGE_HOSTS.has(hostname.toLowerCase());
|
||||
return LOCAL_IMAGE_HOSTS.has(hostname.toLowerCase())
|
||||
}
|
||||
|
||||
export function directNetworkPicUrl(sourceUrl: string): string | null {
|
||||
const trimmed = sourceUrl.trim();
|
||||
const trimmed = sourceUrl.trim()
|
||||
if (!trimmed || /^(data|blob):/i.test(trimmed) || /^\/(?!\/)/.test(trimmed)) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
const normalized = normalizeRemoteUrl(trimmed);
|
||||
const normalized = normalizeRemoteUrl(trimmed)
|
||||
if (!normalized || /^(data|blob):/i.test(normalized)) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = new URL(normalized);
|
||||
if (!["http:", "https:"].includes(parsed.protocol) || isLocalImageHost(parsed.hostname)) {
|
||||
return null;
|
||||
const parsed = new URL(normalized)
|
||||
if (!['http:', 'https:'].includes(parsed.protocol) || isLocalImageHost(parsed.hostname)) {
|
||||
return null
|
||||
}
|
||||
return parsed.toString();
|
||||
return parsed.toString()
|
||||
} catch {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,14 +149,14 @@ async function zolHeaders(
|
||||
context: PublishAdapterContext,
|
||||
extra: Record<string, string> = {},
|
||||
): Promise<Record<string, string>> {
|
||||
const cookie = await sessionCookieHeader(context.session, "zol.com.cn").catch(() => "");
|
||||
const cookie = await sessionCookieHeader(context.session, 'zol.com.cn').catch(() => '')
|
||||
return {
|
||||
accept: "application/json, text/plain, */*",
|
||||
accept: 'application/json, text/plain, */*',
|
||||
origin: ZOL_REFERER,
|
||||
referer: ZOL_REFERER,
|
||||
...(cookie ? { cookie } : {}),
|
||||
...extra,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAccount(context: PublishAdapterContext): Promise<ZolAccount | null> {
|
||||
@@ -162,46 +164,46 @@ async function fetchAccount(context: PublishAdapterContext): Promise<ZolAccount
|
||||
context.session,
|
||||
`${ZOL_API_ORIGIN}/api/v1/creator.user.getinfo`,
|
||||
{
|
||||
credentials: "include",
|
||||
credentials: 'include',
|
||||
headers: await zolHeaders(context),
|
||||
},
|
||||
).catch(() => null);
|
||||
).catch(() => null)
|
||||
|
||||
const user = response?.data;
|
||||
const uid = stringId(user?.userId);
|
||||
const name = user?.nickName?.trim() ?? "";
|
||||
return uid && name ? { uid, name } : null;
|
||||
const user = response?.data
|
||||
const uid = stringId(user?.userId)
|
||||
const name = user?.nickName?.trim() ?? ''
|
||||
return uid && name ? { uid, name } : null
|
||||
}
|
||||
|
||||
function appendBaseFormFields(form: FormData, title: string, userId: string): void {
|
||||
form.append("businessType", "1");
|
||||
form.append("scontent", "");
|
||||
form.append("title", title);
|
||||
form.append("stitle", "");
|
||||
form.append("userId", userId);
|
||||
form.append("docType", "");
|
||||
form.append("isOriginal", "");
|
||||
form.append("isContribution", "0");
|
||||
form.append("dutyEditor", "undefined");
|
||||
form.append("publishDate", "");
|
||||
form.append("subjectList", "");
|
||||
form.append("subjectIdStr", "");
|
||||
form.append("guideImg", "");
|
||||
form.append("draftId", "");
|
||||
form.append("contentId", "");
|
||||
form.append("tryId", "");
|
||||
form.append("goodsList", "");
|
||||
form.append("subjectNameStr", "");
|
||||
form.append("eosXuanti", "0");
|
||||
form.append("eosDawen", "0");
|
||||
form.append("eosUser", "0");
|
||||
form.append("eosTeyue", "0");
|
||||
form.append("firstEc", "0");
|
||||
form.append("firstEcForm", "false");
|
||||
form.append("eosSyXuanti", "0");
|
||||
form.append("eosGEO", "0");
|
||||
form.append("eosZiZhu", "0");
|
||||
form.append("saveType", "2");
|
||||
form.append('businessType', '1')
|
||||
form.append('scontent', '')
|
||||
form.append('title', title)
|
||||
form.append('stitle', '')
|
||||
form.append('userId', userId)
|
||||
form.append('docType', '')
|
||||
form.append('isOriginal', '')
|
||||
form.append('isContribution', '0')
|
||||
form.append('dutyEditor', 'undefined')
|
||||
form.append('publishDate', '')
|
||||
form.append('subjectList', '')
|
||||
form.append('subjectIdStr', '')
|
||||
form.append('guideImg', '')
|
||||
form.append('draftId', '')
|
||||
form.append('contentId', '')
|
||||
form.append('tryId', '')
|
||||
form.append('goodsList', '')
|
||||
form.append('subjectNameStr', '')
|
||||
form.append('eosXuanti', '0')
|
||||
form.append('eosDawen', '0')
|
||||
form.append('eosUser', '0')
|
||||
form.append('eosTeyue', '0')
|
||||
form.append('firstEc', '0')
|
||||
form.append('firstEcForm', 'false')
|
||||
form.append('eosSyXuanti', '0')
|
||||
form.append('eosGEO', '0')
|
||||
form.append('eosZiZhu', '0')
|
||||
form.append('saveType', '2')
|
||||
}
|
||||
|
||||
async function createDraftId(context: PublishAdapterContext, form: FormData): Promise<string> {
|
||||
@@ -209,21 +211,23 @@ async function createDraftId(context: PublishAdapterContext, form: FormData): Pr
|
||||
context.session,
|
||||
`${ZOL_API_ORIGIN}/api/v1/creator.content.draft.save.orther`,
|
||||
{
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: await zolHeaders(context),
|
||||
body: form,
|
||||
},
|
||||
).catch((error): ZolDraftResponse => ({
|
||||
errcode: -1,
|
||||
errmsg: error instanceof Error ? error.message : "zol_draft_create_failed",
|
||||
}));
|
||||
).catch(
|
||||
(error): ZolDraftResponse => ({
|
||||
errcode: -1,
|
||||
errmsg: error instanceof Error ? error.message : 'zol_draft_create_failed',
|
||||
}),
|
||||
)
|
||||
|
||||
const draftId = stringId(response.data?.draftId);
|
||||
const draftId = stringId(response.data?.draftId)
|
||||
if (!draftId) {
|
||||
throw new Error(`zol_draft_create_failed:${zolResponseMessage(response)}`);
|
||||
throw new Error(`zol_draft_create_failed:${zolResponseMessage(response)}`)
|
||||
}
|
||||
return draftId;
|
||||
return draftId
|
||||
}
|
||||
|
||||
async function uploadCoverVariant(
|
||||
@@ -231,66 +235,74 @@ async function uploadCoverVariant(
|
||||
source: ImageAssetBlob,
|
||||
ratio: number,
|
||||
): Promise<ZolCoverImage | null> {
|
||||
const image = await cropImageAssetBlob(source, ratio);
|
||||
const image = await cropImageAssetBlob(source, ratio)
|
||||
if (!image) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
const form = new FormData();
|
||||
form.append("file", image.blob, image.fileName);
|
||||
form.append("siteType", "0");
|
||||
const form = new FormData()
|
||||
form.append('file', image.blob, image.fileName)
|
||||
form.append('siteType', '0')
|
||||
|
||||
const response = await sessionFetchJson<ZolImageUploadResponse>(
|
||||
context.session,
|
||||
`${ZOL_API_ORIGIN}/api/v1/creator.content.image.upload`,
|
||||
{
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: await zolHeaders(context),
|
||||
body: form,
|
||||
},
|
||||
).catch((error): ZolImageUploadResponse => ({
|
||||
errcode: -1,
|
||||
errmsg: error instanceof Error ? error.message : "zol_cover_upload_failed",
|
||||
}));
|
||||
).catch(
|
||||
(error): ZolImageUploadResponse => ({
|
||||
errcode: -1,
|
||||
errmsg: error instanceof Error ? error.message : 'zol_cover_upload_failed',
|
||||
}),
|
||||
)
|
||||
|
||||
const url = response.data?.fileUrl?.trim() || "";
|
||||
const url = response.data?.fileUrl?.trim() || ''
|
||||
if (response.errcode !== 0 || !url) {
|
||||
const message = zolResponseMessage(response);
|
||||
const message = zolResponseMessage(response)
|
||||
if (isZolChallengeMessage(message)) {
|
||||
throw new Error(`zol_challenge_required:${message}`);
|
||||
throw new Error(`zol_challenge_required:${message}`)
|
||||
}
|
||||
throw new Error(`zol_cover_upload_failed:${message}`);
|
||||
throw new Error(`zol_cover_upload_failed:${message}`)
|
||||
}
|
||||
|
||||
return {
|
||||
url,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadCover(context: PublishAdapterContext, sourceUrl: string): Promise<ZolCoverImage[] | null> {
|
||||
const source = await fetchImageAssetBlob(sourceUrl);
|
||||
async function uploadCover(
|
||||
context: PublishAdapterContext,
|
||||
sourceUrl: string,
|
||||
): Promise<ZolCoverImage[] | null> {
|
||||
const source = await fetchImageAssetBlob(sourceUrl)
|
||||
if (!source) {
|
||||
throw new Error("zol_cover_fetch_failed");
|
||||
throw new Error('zol_cover_fetch_failed')
|
||||
}
|
||||
|
||||
const landscape = await uploadCoverVariant(context, source, 4 / 3);
|
||||
const portrait = await uploadCoverVariant(context, source, 3 / 4);
|
||||
return landscape && portrait ? [landscape, portrait] : null;
|
||||
const landscape = await uploadCoverVariant(context, source, 4 / 3)
|
||||
const portrait = await uploadCoverVariant(context, source, 3 / 4)
|
||||
return landscape && portrait ? [landscape, portrait] : null
|
||||
}
|
||||
|
||||
async function uploadContentImage(context: PublishAdapterContext, sourceUrl: string): Promise<string | null> {
|
||||
const networkPic = directNetworkPicUrl(sourceUrl);
|
||||
async function uploadContentImage(
|
||||
context: PublishAdapterContext,
|
||||
sourceUrl: string,
|
||||
): Promise<string | null> {
|
||||
const networkPic = directNetworkPicUrl(sourceUrl)
|
||||
if (networkPic) {
|
||||
const uploaded = await uploadContentImageByNetworkPic(context, networkPic);
|
||||
const uploaded = await uploadContentImageByNetworkPic(context, networkPic)
|
||||
if (uploaded) {
|
||||
return uploaded;
|
||||
return uploaded
|
||||
}
|
||||
}
|
||||
|
||||
return await uploadContentImageByFile(context, sourceUrl);
|
||||
return await uploadContentImageByFile(context, sourceUrl)
|
||||
}
|
||||
|
||||
async function uploadContentImageByNetworkPic(
|
||||
@@ -298,306 +310,316 @@ async function uploadContentImageByNetworkPic(
|
||||
networkPic: string,
|
||||
): Promise<string | null> {
|
||||
if (!networkPic) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
const form = new FormData();
|
||||
form.append("networkPic", networkPic);
|
||||
form.append("siteType", "1");
|
||||
const form = new FormData()
|
||||
form.append('networkPic', networkPic)
|
||||
form.append('siteType', '1')
|
||||
|
||||
const response = await sessionFetchJson<ZolImageUploadResponse>(
|
||||
context.session,
|
||||
`${ZOL_API_ORIGIN}/api/v1/creator.content.image.upload`,
|
||||
{
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: await zolHeaders(context),
|
||||
body: form,
|
||||
},
|
||||
).catch(() => null);
|
||||
).catch(() => null)
|
||||
|
||||
const uploaded = zolUploadedImageUrl(response);
|
||||
const uploaded = zolUploadedImageUrl(response)
|
||||
if (response && response.errcode !== 0 && response.errcode != null) {
|
||||
const message = zolResponseMessage(response);
|
||||
const message = zolResponseMessage(response)
|
||||
if (isZolChallengeMessage(message)) {
|
||||
throw new Error(`zol_challenge_required:${message}`);
|
||||
throw new Error(`zol_challenge_required:${message}`)
|
||||
}
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
return uploaded;
|
||||
return uploaded
|
||||
}
|
||||
|
||||
async function uploadContentImageByFile(context: PublishAdapterContext, sourceUrl: string): Promise<string | null> {
|
||||
const image = await fetchImageAssetBlob(sourceUrl);
|
||||
async function uploadContentImageByFile(
|
||||
context: PublishAdapterContext,
|
||||
sourceUrl: string,
|
||||
): Promise<string | null> {
|
||||
const image = await fetchImageAssetBlob(sourceUrl)
|
||||
if (!image) {
|
||||
throw new Error(`zol_content_image_upload_failed:${sourceUrl.slice(0, 160)}:source_fetch_failed`);
|
||||
throw new Error(
|
||||
`zol_content_image_upload_failed:${sourceUrl.slice(0, 160)}:source_fetch_failed`,
|
||||
)
|
||||
}
|
||||
|
||||
const errors: string[] = [];
|
||||
for (const siteType of ["1", "0"]) {
|
||||
const form = new FormData();
|
||||
form.append("file", image.blob, image.fileName);
|
||||
form.append("siteType", siteType);
|
||||
const errors: string[] = []
|
||||
for (const siteType of ['1', '0']) {
|
||||
const form = new FormData()
|
||||
form.append('file', image.blob, image.fileName)
|
||||
form.append('siteType', siteType)
|
||||
|
||||
const response = await sessionFetchJson<ZolImageUploadResponse>(
|
||||
context.session,
|
||||
`${ZOL_API_ORIGIN}/api/v1/creator.content.image.upload`,
|
||||
{
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: await zolHeaders(context),
|
||||
body: form,
|
||||
},
|
||||
).catch((error): ZolImageUploadResponse => ({
|
||||
errcode: -1,
|
||||
errmsg: error instanceof Error ? error.message : "zol_content_image_upload_failed",
|
||||
}));
|
||||
).catch(
|
||||
(error): ZolImageUploadResponse => ({
|
||||
errcode: -1,
|
||||
errmsg: error instanceof Error ? error.message : 'zol_content_image_upload_failed',
|
||||
}),
|
||||
)
|
||||
|
||||
const uploaded = zolUploadedImageUrl(response);
|
||||
const uploaded = zolUploadedImageUrl(response)
|
||||
if (uploaded) {
|
||||
return uploaded;
|
||||
return uploaded
|
||||
}
|
||||
|
||||
const message = zolImageUploadMessage(response);
|
||||
const message = zolImageUploadMessage(response)
|
||||
if (isZolChallengeMessage(message)) {
|
||||
throw new Error(`zol_challenge_required:${message}`);
|
||||
throw new Error(`zol_challenge_required:${message}`)
|
||||
}
|
||||
errors.push(`siteType=${siteType}:${message}`);
|
||||
errors.push(`siteType=${siteType}:${message}`)
|
||||
}
|
||||
|
||||
throw new Error(`zol_content_image_upload_failed:${sourceUrl.slice(0, 160)}:${errors.join(";")}`);
|
||||
throw new Error(`zol_content_image_upload_failed:${sourceUrl.slice(0, 160)}:${errors.join(';')}`)
|
||||
}
|
||||
|
||||
async function uploadContentImages(context: PublishAdapterContext, html: string): Promise<string> {
|
||||
const sources = extractImageSources(html);
|
||||
const sources = extractImageSources(html)
|
||||
if (!sources.length) {
|
||||
return html;
|
||||
return html
|
||||
}
|
||||
|
||||
const uploaded = new Map<string, string>();
|
||||
const uploaded = new Map<string, string>()
|
||||
for (const source of sources) {
|
||||
const target = await uploadContentImage(context, source);
|
||||
const target = await uploadContentImage(context, source)
|
||||
if (!target) {
|
||||
throw new Error(`zol_content_image_upload_failed:${source.slice(0, 160)}`);
|
||||
throw new Error(`zol_content_image_upload_failed:${source.slice(0, 160)}`)
|
||||
}
|
||||
uploaded.set(source, target);
|
||||
uploaded.set(source, target)
|
||||
}
|
||||
|
||||
let next = html;
|
||||
let next = html
|
||||
for (const [source, target] of uploaded.entries()) {
|
||||
next = next.split(source).join(target);
|
||||
next = next.split(source).join(target)
|
||||
}
|
||||
|
||||
return next;
|
||||
return next
|
||||
}
|
||||
|
||||
async function applyRecommendSubjects(context: PublishAdapterContext, form: FormData): Promise<void> {
|
||||
const url = new URL(`${ZOL_API_ORIGIN}/api/v1/creator.comminuty.subjectlist`);
|
||||
url.searchParams.set("sa", "pc");
|
||||
url.searchParams.set("keyword", "");
|
||||
url.searchParams.set("page", "1");
|
||||
async function applyRecommendSubjects(
|
||||
context: PublishAdapterContext,
|
||||
form: FormData,
|
||||
): Promise<void> {
|
||||
const url = new URL(`${ZOL_API_ORIGIN}/api/v1/creator.comminuty.subjectlist`)
|
||||
url.searchParams.set('sa', 'pc')
|
||||
url.searchParams.set('keyword', '')
|
||||
url.searchParams.set('page', '1')
|
||||
|
||||
const response = await sessionFetchJson<ZolSubjectListResponse>(
|
||||
context.session,
|
||||
url.toString(),
|
||||
{
|
||||
credentials: "include",
|
||||
headers: await zolHeaders(context),
|
||||
},
|
||||
).catch(() => null);
|
||||
const response = await sessionFetchJson<ZolSubjectListResponse>(context.session, url.toString(), {
|
||||
credentials: 'include',
|
||||
headers: await zolHeaders(context),
|
||||
}).catch(() => null)
|
||||
|
||||
const subjects = (response?.data?.list ?? [])
|
||||
.filter((item) => item.subjectId != null && item.subjectName?.trim())
|
||||
.slice(0, ZOL_SUBJECT_LIMIT);
|
||||
.slice(0, ZOL_SUBJECT_LIMIT)
|
||||
if (!subjects.length) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
form.set("subjectIdStr", subjects.map((item) => String(item.subjectId)).join(","));
|
||||
form.set("subjectNameStr", subjects.map((item) => item.subjectName?.trim() ?? "").join(","));
|
||||
form.set('subjectIdStr', subjects.map((item) => String(item.subjectId)).join(','))
|
||||
form.set('subjectNameStr', subjects.map((item) => item.subjectName?.trim() ?? '').join(','))
|
||||
}
|
||||
|
||||
function buildResultPayload(articleId: string, mediaName: string): Record<string, JsonValue> {
|
||||
return {
|
||||
platform: "zol",
|
||||
platform: 'zol',
|
||||
media_name: mediaName,
|
||||
external_article_id: articleId,
|
||||
external_manage_url: ZOL_MANAGE_URL,
|
||||
external_article_url: null,
|
||||
publish_type: "publish",
|
||||
};
|
||||
publish_type: 'publish',
|
||||
}
|
||||
}
|
||||
|
||||
function failureResult(error: unknown): ReturnType<PublishAdapter["publish"]> extends Promise<infer T> ? T : never {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
function failureResult(
|
||||
error: unknown,
|
||||
): ReturnType<PublishAdapter['publish']> extends Promise<infer T> ? T : never {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
|
||||
if (message === "zol_not_logged_in") {
|
||||
if (message === 'zol_not_logged_in') {
|
||||
return {
|
||||
status: "failed",
|
||||
summary: "中关村在线登录态失效,无法执行发布。",
|
||||
status: 'failed',
|
||||
summary: '中关村在线登录态失效,无法执行发布。',
|
||||
error: {
|
||||
code: "zol_not_logged_in",
|
||||
code: 'zol_not_logged_in',
|
||||
message,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (message === "article_content_empty") {
|
||||
if (message === 'article_content_empty') {
|
||||
return {
|
||||
status: "failed",
|
||||
summary: "文章内容为空,无法推送到中关村在线。",
|
||||
status: 'failed',
|
||||
summary: '文章内容为空,无法推送到中关村在线。',
|
||||
error: {
|
||||
code: "article_content_empty",
|
||||
code: 'article_content_empty',
|
||||
message,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (message.startsWith("zol_draft_create_failed")) {
|
||||
if (message.startsWith('zol_draft_create_failed')) {
|
||||
return {
|
||||
status: "failed",
|
||||
summary: "中关村在线草稿 ID 创建失败,请重新打开后台确认账号状态。",
|
||||
status: 'failed',
|
||||
summary: '中关村在线草稿 ID 创建失败,请重新打开后台确认账号状态。',
|
||||
error: {
|
||||
code: "zol_draft_create_failed",
|
||||
code: 'zol_draft_create_failed',
|
||||
message,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (message.startsWith("zol_cover_fetch_failed")) {
|
||||
if (message.startsWith('zol_cover_fetch_failed')) {
|
||||
return {
|
||||
status: "failed",
|
||||
summary: "中关村在线封面文件读取失败,请重新上传封面图后重试。",
|
||||
status: 'failed',
|
||||
summary: '中关村在线封面文件读取失败,请重新上传封面图后重试。',
|
||||
error: {
|
||||
code: "zol_cover_fetch_failed",
|
||||
code: 'zol_cover_fetch_failed',
|
||||
message,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (message.startsWith("zol_cover_upload_failed")) {
|
||||
if (message.startsWith('zol_cover_upload_failed')) {
|
||||
return {
|
||||
status: "failed",
|
||||
summary: "中关村在线封面上传失败,请稍后重试或更换封面图。",
|
||||
status: 'failed',
|
||||
summary: '中关村在线封面上传失败,请稍后重试或更换封面图。',
|
||||
error: {
|
||||
code: "zol_cover_upload_failed",
|
||||
code: 'zol_cover_upload_failed',
|
||||
message,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (message.startsWith("zol_content_image_upload_failed")) {
|
||||
if (message.startsWith('zol_content_image_upload_failed')) {
|
||||
return {
|
||||
status: "failed",
|
||||
summary: "中关村在线正文图片上传失败,已停止发布以避免生成空白图片。",
|
||||
status: 'failed',
|
||||
summary: '中关村在线正文图片上传失败,已停止发布以避免生成空白图片。',
|
||||
error: {
|
||||
code: "zol_content_image_upload_failed",
|
||||
code: 'zol_content_image_upload_failed',
|
||||
message,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (message.startsWith("zol_challenge_required") || isZolChallengeMessage(message)) {
|
||||
if (message.startsWith('zol_challenge_required') || isZolChallengeMessage(message)) {
|
||||
return {
|
||||
status: "failed",
|
||||
summary: "中关村在线触发平台验证,请在中关村在线后台完成验证后重试。",
|
||||
status: 'failed',
|
||||
summary: '中关村在线触发平台验证,请在中关村在线后台完成验证后重试。',
|
||||
error: {
|
||||
code: "zol_challenge_required",
|
||||
code: 'zol_challenge_required',
|
||||
message,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: "failed",
|
||||
summary: "中关村在线发布失败。",
|
||||
status: 'failed',
|
||||
summary: '中关村在线发布失败。',
|
||||
error: {
|
||||
code: "zol_publish_failed",
|
||||
code: 'zol_publish_failed',
|
||||
message,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const zolAdapter: PublishAdapter = {
|
||||
platform: "zol",
|
||||
executionMode: "session",
|
||||
platform: 'zol',
|
||||
executionMode: 'session',
|
||||
async publish(context) {
|
||||
try {
|
||||
context.reportProgress("zol.detect_login");
|
||||
const account = await fetchAccount(context);
|
||||
context.reportProgress('zol.detect_login')
|
||||
const account = await fetchAccount(context)
|
||||
if (!account) {
|
||||
throw new Error("zol_not_logged_in");
|
||||
throw new Error('zol_not_logged_in')
|
||||
}
|
||||
|
||||
let html = normalizeArticleHtml(context.article);
|
||||
let html = normalizeArticleHtml(context.article)
|
||||
if (!html) {
|
||||
throw new Error("article_content_empty");
|
||||
throw new Error('article_content_empty')
|
||||
}
|
||||
|
||||
const title = context.article.title?.trim() || "未命名文章";
|
||||
const form = new FormData();
|
||||
appendBaseFormFields(form, title, account.uid);
|
||||
const title = context.article.title?.trim() || '未命名文章'
|
||||
const form = new FormData()
|
||||
appendBaseFormFields(form, title, account.uid)
|
||||
|
||||
context.reportProgress("zol.create_draft");
|
||||
const draftId = await createDraftId(context, form);
|
||||
context.reportProgress('zol.create_draft')
|
||||
const draftId = await createDraftId(context, form)
|
||||
|
||||
const coverAssetUrl = context.article.cover_asset_url?.trim() || "";
|
||||
const coverAssetUrl = context.article.cover_asset_url?.trim() || ''
|
||||
if (coverAssetUrl) {
|
||||
context.reportProgress("zol.upload_cover");
|
||||
const cover = await uploadCover(context, coverAssetUrl);
|
||||
context.reportProgress('zol.upload_cover')
|
||||
const cover = await uploadCover(context, coverAssetUrl)
|
||||
if (cover) {
|
||||
form.set("guideImg", JSON.stringify(cover));
|
||||
form.set('guideImg', JSON.stringify(cover))
|
||||
}
|
||||
if (extractImageSources(html).length === 0) {
|
||||
html = `<img src="${coverAssetUrl}" />${html}`;
|
||||
html = `<img src="${coverAssetUrl}" />${html}`
|
||||
}
|
||||
}
|
||||
|
||||
context.reportProgress("zol.apply_subjects");
|
||||
await applyRecommendSubjects(context, form);
|
||||
context.reportProgress('zol.apply_subjects')
|
||||
await applyRecommendSubjects(context, form)
|
||||
|
||||
context.reportProgress("zol.upload_content_images");
|
||||
const processedHtml = await uploadContentImages(context, html);
|
||||
context.reportProgress('zol.upload_content_images')
|
||||
const processedHtml = await uploadContentImages(context, html)
|
||||
|
||||
form.set("draftId", draftId);
|
||||
form.set("saveType", "1");
|
||||
form.set("scontent", processedHtml);
|
||||
form.append("draftUpdateId", draftId);
|
||||
form.append("taskType", "1");
|
||||
form.append("taskIds", "[]");
|
||||
form.set('draftId', draftId)
|
||||
form.set('saveType', '1')
|
||||
form.set('scontent', processedHtml)
|
||||
form.append('draftUpdateId', draftId)
|
||||
form.append('taskType', '1')
|
||||
form.append('taskIds', '[]')
|
||||
|
||||
context.reportProgress("zol.submit");
|
||||
context.reportProgress('zol.submit')
|
||||
const response = await sessionFetchJson<ZolPublishResponse>(
|
||||
context.session,
|
||||
`${ZOL_API_ORIGIN}/api/v1/creator.content.save.orther`,
|
||||
{
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: await zolHeaders(context),
|
||||
body: form,
|
||||
},
|
||||
).catch((error): ZolPublishResponse => ({
|
||||
errcode: -1,
|
||||
errmsg: error instanceof Error ? error.message : "zol_publish_failed",
|
||||
}));
|
||||
).catch(
|
||||
(error): ZolPublishResponse => ({
|
||||
errcode: -1,
|
||||
errmsg: error instanceof Error ? error.message : 'zol_publish_failed',
|
||||
}),
|
||||
)
|
||||
|
||||
const articleId = stringId(response.data?.contentId);
|
||||
const articleId = stringId(response.data?.contentId)
|
||||
if (response.errcode !== 0 || !articleId) {
|
||||
const message = zolResponseMessage(response);
|
||||
const message = zolResponseMessage(response)
|
||||
if (isZolChallengeMessage(message)) {
|
||||
throw new Error(`zol_challenge_required:${message}`);
|
||||
throw new Error(`zol_challenge_required:${message}`)
|
||||
}
|
||||
throw new Error(message || "zol_publish_failed");
|
||||
throw new Error(message || 'zol_publish_failed')
|
||||
}
|
||||
|
||||
return {
|
||||
status: "succeeded",
|
||||
status: 'succeeded',
|
||||
payload: buildResultPayload(articleId, account.name),
|
||||
summary: "中关村在线发布成功。",
|
||||
};
|
||||
summary: '中关村在线发布成功。',
|
||||
}
|
||||
} catch (error) {
|
||||
return failureResult(error);
|
||||
return failureResult(error)
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user