import type { JsonValue } from '@geo/shared-types' import type { Page } from 'playwright-core' import { WANGYIHAO_DRAFT_SAVE_PARAMETER_MESSAGE, WANGYIHAO_PUBLISH_CLICK_MESSAGE, WANGYIHAO_TOKEN_MISSING_MESSAGE, } from '../../shared/publisher-errors' import type { PublishAdapter, PublishAdapterContext } from './base' import { extractImageSources, sessionCookieHeader, sessionFetchJson, uploadHtmlImages, } from './common' import { fetchImageAssetBlob } from './media-image' import { prepareWangyihaoArticleHtml } from './wangyihao-content' import { wangyihaoPublicArticleUrl } from './wangyihao-links' type WangyiInfoResponse = { data?: { mediaInfo?: { userId?: string | number wemediaId?: string | number tname?: string icon?: string } } } type WangyiUploadResponse = { code?: number message?: string data?: { url?: string } } type WangyiDraftResponse = { code?: number | string message?: string msg?: string errMsg?: string errorMsg?: string data?: string } type WangyiPublishDetailResponse = { code?: number | string data?: { wemediaId?: string | number onlineState?: string | number hasRecommRight?: boolean | number allowedRepost?: boolean | number newProminentFlag?: string | number } } type WangyiAccount = { realUserId: string wemediaId: string name: string } type WangyiPublishDetail = { wemediaId: string onlineState: string hasRecommRight: boolean allowedRepost: boolean newProminentFlag: string } const WANGYI_API_ORIGIN = 'https://mp.163.com' const WANGYI_CONSOLE_ORIGIN = 'http://mp.163.com' const WANGYI_API_REFERER = `${WANGYI_API_ORIGIN}/subscribe_v4/index.html` const WANGYI_HOME_URL = `${WANGYI_CONSOLE_ORIGIN}/subscribe_v4/index.html` const WANGYI_MANAGE_URL = `${WANGYI_CONSOLE_ORIGIN}/subscribe_v4/index.html#/article-manage` const WANGYI_CREATIVE_STATEMENT = '个人原创,仅供参考' const WANGYI_PUBLISH_DELAY_MS = 2_000 const WANGYI_TOKEN_WAIT_MS = 12_000 const WANGYI_GUARDIAN_APP_ID = 'YD00250021379271' const WANGYI_GUARDIAN_SCRIPT_URL = 'https://static.ws.126.net/163/mp/main/static/YiDunProtector-Web-2.0.7.js' const WANGYI_TOKEN_ATTR = 'data-geo-wangyihao-token' const WANGYI_TOKEN_STATUS_ATTR = 'data-geo-wangyihao-token-status' const WANGYI_TOKEN_ERROR_ATTR = 'data-geo-wangyihao-token-error' const WANGYI_RESPONSE_CODE_MESSAGES: Record = { '100001': '参数为空', '100002': '参数错误', '100003': '数据不存在', '100004': '参数超出范围', '100005': '数据已存在', '100006': '请选择上传文件', } function stringId(value: unknown): string { if (typeof value === 'number') { return Number.isFinite(value) ? String(value) : '' } if (typeof value !== 'string') { return '' } return value.trim() } function parseFormString(value: string): Record { return Object.fromEntries(new URLSearchParams(value)) } function wangyiResponseMessage( response: | { message?: string msg?: string errMsg?: string errorMsg?: string code?: number | string data?: unknown } | null | undefined, ): string { return ( response?.message || response?.msg || response?.errMsg || response?.errorMsg || (typeof response?.data === 'string' && !response.data.includes('=') ? response.data : '') || WANGYI_RESPONSE_CODE_MESSAGES[String(response?.code ?? '')] || `wangyihao_error_${response?.code ?? 'unknown'}` ) } function isWangyiChallengeMessage(message: string): boolean { return /(验证码|滑块|人机验证|安全验证|请完成验证|短信验证|扫码验证|captcha|challenge|verification required|风险|风控)/i.test( message, ) } function sleep(ms: number, signal?: AbortSignal): Promise { if (signal?.aborted) { return Promise.reject(new Error('adapter_aborted')) } return new Promise((resolve, reject) => { const cleanup = () => signal?.removeEventListener('abort', onAbort) const timeout = setTimeout(() => { cleanup() resolve() }, ms) const onAbort = () => { clearTimeout(timeout) cleanup() reject(new Error('adapter_aborted')) } signal?.addEventListener('abort', onAbort, { once: true }) }) } async function wangyiHeaders( context: PublishAdapterContext, extra: Record = {}, ): Promise> { const cookie = (await sessionCookieHeader(context.session, 'mp.163.com').catch(() => '')) || (await sessionCookieHeader(context.session, '163.com').catch(() => '')) return { accept: 'application/json, text/plain, */*', origin: WANGYI_API_ORIGIN, referer: WANGYI_API_REFERER, ...(cookie ? { cookie } : {}), ...extra, } } async function fetchAccount(context: PublishAdapterContext): Promise { const response = await sessionFetchJson( context.session, `${WANGYI_API_ORIGIN}/wemedia/info.do`, { credentials: 'include', headers: await wangyiHeaders(context), }, ).catch(() => null) const media = response?.data?.mediaInfo const realUserId = stringId(media?.userId) const wemediaId = stringId(media?.wemediaId) const name = media?.tname?.trim() || '' return realUserId && wemediaId && name ? { realUserId, wemediaId, name } : null } async function fetchPublishDetail( context: PublishAdapterContext, account: WangyiAccount, ): Promise { const fallback: WangyiPublishDetail = { wemediaId: account.wemediaId, onlineState: '', hasRecommRight: false, allowedRepost: false, newProminentFlag: '', } const url = new URL(`${WANGYI_API_ORIGIN}/wemedia/article/postpage.do`) url.searchParams.set('_', String(Date.now())) url.searchParams.set('wemediaId', account.wemediaId) url.searchParams.set('mediaId', account.wemediaId) const response = await sessionFetchJson( context.session, url.toString(), { credentials: 'include', headers: await wangyiHeaders(context), }, ).catch(() => null) if (String(response?.code) !== '1' || !response?.data) { return fallback } return { wemediaId: stringId(response.data.wemediaId) || account.wemediaId, onlineState: stringId(response.data.onlineState), hasRecommRight: response.data.hasRecommRight === true || response.data.hasRecommRight === 1, allowedRepost: response.data.allowedRepost === true || response.data.allowedRepost === 1, newProminentFlag: stringId(response.data.newProminentFlag), } } async function getPublishToken(context: PublishAdapterContext): Promise { const page = context.playwright?.page if (!page) { throw new Error('wangyihao_playwright_missing') } for (let attempt = 0; attempt < 2; attempt += 1) { await page .goto(WANGYI_HOME_URL, { waitUntil: attempt === 0 ? 'domcontentloaded' : 'load', timeout: 20_000, }) .catch(() => null) await page.waitForLoadState('networkidle', { timeout: 3_000 }).catch(() => null) if (attempt > 0) { await sleep(800, context.signal) } // 网易号的 neg 有时是顶层绑定,不一定挂在 window 上;这里保持和旧版插件一致的读取方式。 const token = (await readPublishTokenFromPage(page)).trim() if (token) { return token } } return '' } async function readPublishTokenFromPage(page: Page): Promise { await page .evaluate( (attrs) => { document.documentElement.removeAttribute(attrs.token) document.documentElement.removeAttribute(attrs.status) document.documentElement.removeAttribute(attrs.error) }, { token: WANGYI_TOKEN_ATTR, status: WANGYI_TOKEN_STATUS_ATTR, error: WANGYI_TOKEN_ERROR_ATTR, }, ) .catch(() => null) await page .addScriptTag({ content: ` (() => { const tokenAttr = ${JSON.stringify(WANGYI_TOKEN_ATTR)}; const statusAttr = ${JSON.stringify(WANGYI_TOKEN_STATUS_ATTR)}; const errorAttr = ${JSON.stringify(WANGYI_TOKEN_ERROR_ATTR)}; const guardianScriptUrl = ${JSON.stringify(WANGYI_GUARDIAN_SCRIPT_URL)}; const guardianAppId = ${JSON.stringify(WANGYI_GUARDIAN_APP_ID)}; const timeoutMs = ${WANGYI_TOKEN_WAIT_MS}; const retryMs = 250; const scriptLoadTimeoutMs = 6_000; const root = document.documentElement; const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const setStatus = (status, error = "") => { root.setAttribute(statusAttr, status); if (error) { root.setAttribute(errorAttr, String(error).slice(0, 240)); } }; const readTopLevelNeg = () => { try { return typeof neg !== "undefined" ? neg : null; } catch (_error) { return null; } }; const readGuardianFactory = () => { try { return typeof createNEGuardian === "function" ? createNEGuardian : null; } catch (_error) { return null; } }; const readGuardian = () => { const topLevelNeg = readTopLevelNeg(); if (topLevelNeg && typeof topLevelNeg.getToken === "function") { return topLevelNeg; } if (window.neg && typeof window.neg.getToken === "function") { return window.neg; } if (window.__geoWangyihaoNeg && typeof window.__geoWangyihaoNeg.getToken === "function") { return window.__geoWangyihaoNeg; } const createGuardian = readGuardianFactory(); if (createGuardian) { window.__geoWangyihaoNeg = createGuardian({ appId: guardianAppId, timeout: 6_000 }); return window.__geoWangyihaoNeg; } return null; }; const loadGuardianScript = () => new Promise((resolve) => { if (readGuardianFactory()) { resolve(true); return; } const script = document.createElement("script"); let done = false; const finish = (loaded) => { if (done) { return; } done = true; resolve(loaded); }; script.src = guardianScriptUrl; script.async = false; script.onload = () => finish(true); script.onerror = () => finish(false); setTimeout(() => finish(false), scriptLoadTimeoutMs); document.head.appendChild(script); }); const requestToken = async (client) => { const result = await Promise.resolve(client.getToken()); return typeof result?.token === "string" ? result.token.trim() : ""; }; window.__geoWangyihaoTokenPromise = (async () => { const deadline = Date.now() + timeoutMs; setStatus("waiting"); if (document.readyState === "loading") { await new Promise((resolve) => { document.addEventListener("DOMContentLoaded", resolve, { once: true }); }); } if (!readGuardian()) { setStatus("loading_guardian"); await loadGuardianScript(); } while (Date.now() <= deadline) { const client = readGuardian(); if (!client) { setStatus("missing_client"); await sleep(retryMs); continue; } try { setStatus("requesting"); const token = await requestToken(client); if (token) { root.setAttribute(tokenAttr, token); setStatus("ready"); return token; } setStatus("empty_token"); } catch (error) { setStatus("retrying", error && error.message ? error.message : error); } await sleep(retryMs); } setStatus("missing"); return ""; })().catch((error) => { setStatus("failed", error && error.message ? error.message : error); return ""; }); })(); `, }) .catch(() => null) await page .waitForFunction( ({ token, status }) => { const root = document.documentElement const statusValue = root.getAttribute(status) return ( Boolean(root.getAttribute(token)) || statusValue === 'missing' || statusValue === 'failed' ) }, { token: WANGYI_TOKEN_ATTR, status: WANGYI_TOKEN_STATUS_ATTR, }, { timeout: WANGYI_TOKEN_WAIT_MS + 2_000 }, ) .catch(() => null) const token = await page .evaluate((attr) => document.documentElement.getAttribute(attr) ?? '', WANGYI_TOKEN_ATTR) .catch(() => '') await page .evaluate( (attrs) => { document.documentElement.removeAttribute(attrs.token) document.documentElement.removeAttribute(attrs.status) document.documentElement.removeAttribute(attrs.error) }, { token: WANGYI_TOKEN_ATTR, status: WANGYI_TOKEN_STATUS_ATTR, error: WANGYI_TOKEN_ERROR_ATTR, }, ) .catch(() => null) return token } async function uploadImage( context: PublishAdapterContext, account: WangyiAccount, sourceUrl: string, ): Promise { const image = await fetchImageAssetBlob(sourceUrl) if (!image) { return null } const form = new FormData() form.append('file', image.blob, image.fileName) form.append('from', 'neteasecode_mp') form.append('logotext', account.name) const url = new URL(`${WANGYI_API_ORIGIN}/api/v3/upload/picupload`) url.searchParams.set('_', String(Date.now())) url.searchParams.set('wemediaId', account.wemediaId) url.searchParams.set('realUserId', account.realUserId) const response = await sessionFetchJson(context.session, url.toString(), { method: 'POST', credentials: 'include', headers: await wangyiHeaders(context, { 'x-b3-sampled': '1', 'x-b3-spanid': '0', }), body: form, }).catch(() => null) return response?.data?.url?.trim() || null } async function saveDraft( context: PublishAdapterContext, account: WangyiAccount, detail: WangyiPublishDetail, html: string, token: string, ): Promise { const url = new URL(`${WANGYI_API_ORIGIN}/wemedia/article/status/api/publishV2.do`) url.searchParams.set('_', String(Date.now())) const body = buildDraftBody(context, account, detail, html, token) const response = (await saveDraftWithPage(context, url.toString(), body).catch(() => null)) ?? (await saveDraftWithSession(context, url.toString(), body)) if (String(response.code) !== '1' || !response.data) { const message = wangyiResponseMessage(response) if (isWangyiChallengeMessage(message)) { throw new Error(`wangyihao_challenge_required:${message}`) } throw new Error(`wangyihao_draft_save_failed:${message}`) } const draftId = parseFormString(response.data).docId ?? '' if (!draftId.trim()) { throw new Error('wangyihao_draft_id_missing') } return draftId.trim() } function buildDraftBody( context: PublishAdapterContext, account: WangyiAccount, detail: WangyiPublishDetail, html: string, token: string, ): URLSearchParams { const body = new URLSearchParams() body.set('wemediaId', detail.wemediaId || account.wemediaId) body.set('articleId', '-1') body.set('title', context.article.title?.trim() || '未命名文章') body.set('content', html) body.set('cover', 'auto') body.set('operation', 'saveDraft') body.set('scheduled', '0') body.set('ursToken', token) body.set('picUrl', '') body.set('original', '0') body.set('creativeStatement', WANGYI_CREATIVE_STATEMENT) if (detail.onlineState) { body.set('onlineState', detail.onlineState) } if (detail.hasRecommRight) { body.set('recommendState', '0') } if (detail.allowedRepost) { body.set('allowedRepost', '1') } if (detail.newProminentFlag === '1') { body.set('newProminentState', '0') } return body } async function saveDraftWithSession( context: PublishAdapterContext, url: string, body: URLSearchParams, ): Promise { return await sessionFetchJson(context.session, url, { method: 'POST', credentials: 'include', headers: await wangyiHeaders(context, { 'content-type': 'application/x-www-form-urlencoded', 'x-b3-sampled': '1', 'x-b3-spanid': '0', }), body, }).catch( (error): WangyiDraftResponse => ({ code: -1, message: error instanceof Error ? error.message : 'wangyihao_draft_save_failed', }), ) } async function saveDraftWithPage( context: PublishAdapterContext, url: string, body: URLSearchParams, ): Promise { const page = context.playwright?.page if (!page) { return null } const serialized = await page.evaluate( async ({ requestUrl, requestBody }) => { const response = await fetch(requestUrl, { method: 'POST', credentials: 'include', headers: { 'content-type': 'application/x-www-form-urlencoded', 'x-b3-sampled': '1', 'x-b3-spanid': '0', }, body: requestBody, }) return await response.text() }, { requestUrl: url, requestBody: body.toString(), }, ) if (!serialized) { return null } return JSON.parse(serialized) as WangyiDraftResponse } type WangyiPublishNetworkResult = { success: boolean code: string message: string } function compactText(value: string): string { return value.trim().replace(/\s+/g, '') } function isPublishRequestBody(postData: string): boolean { if (!postData) { return false } const decoded = (() => { try { return decodeURIComponent(postData) } catch { return postData } })() return /(?:^|&)operation=publish(?:&|$)/.test(decoded) } async function clickFirstVisibleAction( page: Page, texts: string[], timeoutMs = 10_000, ): Promise { const selectors = [ '.ant-modal button', '.ant-popover button', 'button', "[role='button']", '.ant-btn', 'a', ] const targets = texts.map(compactText) const startedAt = Date.now() while (Date.now() - startedAt < timeoutMs) { for (const selector of selectors) { const locator = page.locator(selector) const count = await locator.count().catch(() => 0) for (let index = 0; index < count; index += 1) { const candidate = locator.nth(index) const text = await candidate .innerText({ timeout: 300 }) .catch(async () => await candidate.textContent({ timeout: 300 }).catch(() => '')) if (!targets.includes(compactText(text || ''))) { continue } const visible = await candidate.isVisible().catch(() => false) const enabled = await candidate.isEnabled().catch(() => false) if (!visible || !enabled) { continue } await candidate.scrollIntoViewIfNeeded({ timeout: 1_000 }).catch(() => undefined) await candidate.click({ timeout: 5_000 }) return true } } const clicked = await page .evaluate((targetTexts) => { const compact = (value: string) => value.trim().replace(/\s+/g, '') const isVisible = (element: Element) => { const rect = element.getBoundingClientRect() if (rect.width <= 0 || rect.height <= 0) { return false } const style = window.getComputedStyle(element) return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0' } const isDisabled = (element: Element) => { const htmlElement = element as HTMLElement & { disabled?: boolean } return ( Boolean(htmlElement.disabled) || element.getAttribute('aria-disabled') === 'true' || /\b(disabled|is-disabled|ant-btn-disabled)\b/i.test(element.className?.toString() || '') ) } const candidates = Array.from( document.querySelectorAll( ".ant-modal button, .ant-popover button, button, [role='button'], .ant-btn, a", ), ) for (const candidate of candidates) { if (!targetTexts.includes(compact(candidate.innerText || candidate.textContent || ''))) { continue } const clickable = candidate.closest("button, [role='button'], a, .ant-btn") ?? candidate if (!isVisible(clickable) || isDisabled(clickable)) { continue } clickable.scrollIntoView({ block: 'center', inline: 'center' }) clickable.click() return true } return false }, targets) .catch(() => false) if (clicked) { return true } await page.waitForTimeout(150).catch(() => undefined) } return false } async function waitForWangyiEditorReady(page: Page): Promise { await page .waitForFunction( () => { if (document.readyState === 'loading') { return false } const actions = Array.from( document.querySelectorAll("button, [role='button'], .ant-btn, a"), ) return actions.some( (element) => (element.textContent || '').trim().replace(/\s+/g, '') === '发布', ) }, undefined, { timeout: 20_000 }, ) .catch(() => undefined) } async function waitForPublishNetworkResponse( page: Page, timeoutMs: number, ): Promise { const response = await page .waitForResponse( (candidate) => { if (!candidate.url().includes('/wemedia/article/status/api/publishV2.do')) { return false } if (candidate.request().method().toUpperCase() !== 'POST') { return false } return isPublishRequestBody(candidate.request().postData() || '') }, { timeout: timeoutMs }, ) .catch(() => null) if (!response) { return null } const text = await response.text().catch(() => '') let parsed: WangyiDraftResponse | null = null try { parsed = text ? (JSON.parse(text) as WangyiDraftResponse) : null } catch { parsed = { code: response.status(), message: text || `http_${response.status()}`, } } return { success: String(parsed?.code) === '1', code: String(parsed?.code ?? response.status()), message: wangyiResponseMessage(parsed), } } async function readWangyiPageMessage(page: Page): Promise { return await page .evaluate(() => { const text = document.body?.innerText || '' return text .split('\n') .map((line) => line.trim()) .filter((line) => /(发布|发表|提交|审核|成功|失败|错误|验证|登录|确认)/.test(line)) .slice(-8) .join(' / ') .slice(0, 300) }) .catch(() => '') } async function waitForPublishCompletion( page: Page, oldUrl: string, responsePromise: Promise, timeoutMs: number, ): Promise { const result = await Promise.race([ responsePromise.then((response) => (response ? { kind: 'network' as const, response } : null)), page .waitForURL((url) => url.toString() !== oldUrl, { timeout: timeoutMs }) .then(() => ({ kind: 'navigation' as const })) .catch(() => null), page .waitForFunction( () => /(发布成功|发表成功|提交成功|已提交审核|已进入审核|审核中)/.test( document.body?.innerText || '', ), undefined, { timeout: timeoutMs }, ) .then(() => ({ kind: 'page' as const })) .catch(() => null), page.waitForTimeout(timeoutMs).then(() => null), ]) if (!result) { return false } if (result.kind === 'network') { if (result.response.success) { return true } throw new Error(`wangyihao_publish_failed:${result.response.message || result.response.code}`) } return true } async function publishDraft(context: PublishAdapterContext, draftId: string): Promise { const page = context.playwright?.page if (!page) { throw new Error('wangyihao_playwright_missing') } const draftUrl = `${WANGYI_CONSOLE_ORIGIN}/subscribe_v4/index.html#/article-publish/${encodeURIComponent(draftId)}?option=editDraft` await page.goto(draftUrl, { waitUntil: 'domcontentloaded', }) await page.waitForLoadState('networkidle', { timeout: 8_000 }).catch(() => null) await waitForWangyiEditorReady(page) const oldUrl = page.url() const publishResponsePromise = waitForPublishNetworkResponse(page, 22_000) const firstClicked = await clickFirstVisibleAction(page, ['发布'], 12_000) if (!firstClicked) { const message = await readWangyiPageMessage(page) throw new Error(`wangyihao_publish_click_failed${message ? `:${message}` : ''}`) } await sleep(WANGYI_PUBLISH_DELAY_MS, context.signal) if (await waitForPublishCompletion(page, oldUrl, publishResponsePromise, 2_500)) { return true } const confirmed = await clickFirstVisibleAction( page, ['确认发布', '确定发布', '确认', '确定'], 8_000, ) if (!confirmed) { return await waitForPublishCompletion(page, oldUrl, publishResponsePromise, 4_000) } return ( (await waitForPublishCompletion(page, oldUrl, publishResponsePromise, 10_000)) || page.url() !== oldUrl ) } function buildResultPayload(articleId: string, mediaName: string): Record { return { platform: 'wangyihao', media_name: mediaName, external_article_id: articleId, external_manage_url: WANGYI_MANAGE_URL, external_article_url: wangyihaoPublicArticleUrl(articleId), publish_type: 'publish', } } function failureResult( error: unknown, ): ReturnType extends Promise ? T : never { const message = error instanceof Error ? error.message : String(error) if (message === 'wangyihao_not_logged_in') { return { status: 'failed', summary: '网易号登录态失效,无法执行发布。', error: { code: 'wangyihao_not_logged_in', message, }, } } if (message === 'article_content_empty') { return { status: 'failed', summary: '文章内容为空,无法推送到网易号。', error: { code: 'article_content_empty', message, }, } } if (message === 'wangyihao_token_missing') { return { status: 'failed', summary: WANGYIHAO_TOKEN_MISSING_MESSAGE, error: { code: 'wangyihao_token_missing', message: WANGYIHAO_TOKEN_MISSING_MESSAGE, }, } } if (message.startsWith('wangyihao_challenge_required') || isWangyiChallengeMessage(message)) { return { status: 'failed', summary: '网易号触发平台验证,请在网易号后台完成验证后重试。', error: { code: 'wangyihao_challenge_required', message, }, } } if ( message.startsWith('wangyihao_draft_save_failed') || message === 'wangyihao_draft_id_missing' ) { const isParameterError = /(?:wangyihao_error_)?100002|参数错误/.test(message) return { status: 'failed', summary: isParameterError ? WANGYIHAO_DRAFT_SAVE_PARAMETER_MESSAGE : '网易号草稿保存失败,请稍后重试或打开网易号后台确认内容状态。', error: { code: 'wangyihao_draft_save_failed', message: isParameterError ? WANGYIHAO_DRAFT_SAVE_PARAMETER_MESSAGE : message, }, } } if (message.startsWith('wangyihao_publish_click_failed')) { return { status: 'failed', summary: WANGYIHAO_PUBLISH_CLICK_MESSAGE, error: { code: 'wangyihao_publish_click_failed', message: WANGYIHAO_PUBLISH_CLICK_MESSAGE, }, } } return { status: 'failed', summary: '网易号发布失败。', error: { code: 'wangyihao_publish_failed', message, }, } } export const wangyihaoAdapter: PublishAdapter = { platform: 'wangyihao', executionMode: 'playwright', async publish(context) { try { context.reportProgress('wangyihao.detect_login') const account = await fetchAccount(context) if (!account) { throw new Error('wangyihao_not_logged_in') } const detail = await fetchPublishDetail(context, account) context.reportProgress('wangyihao.normalize_html') let html = prepareWangyihaoArticleHtml(context.article) if (!html) { throw new Error('article_content_empty') } const coverAssetUrl = context.article.cover_asset_url?.trim() || '' if (coverAssetUrl && extractImageSources(html).length === 0) { html = `${html}` } context.reportProgress('wangyihao.get_token') const token = await getPublishToken(context) if (!token) { throw new Error('wangyihao_token_missing') } context.reportProgress('wangyihao.upload_content_images') const processed = await uploadHtmlImages(html, async (sourceUrl) => uploadImage(context, account, sourceUrl), ) context.reportProgress('wangyihao.save_draft') const draftId = await saveDraft(context, account, detail, processed.html, token) context.reportProgress('wangyihao.publish_draft') const published = await publishDraft(context, draftId) if (!published) { throw new Error('wangyihao_publish_click_failed') } return { status: 'succeeded', summary: '网易号发布成功,已记录公开链接。', payload: buildResultPayload(draftId, account.name), } } catch (error) { return failureResult(error) } }, }