import { browser } from "wxt/browser"; import md5Lib from "js-md5"; import { marked } from "marked"; import type { StoredPlatformState } from "../platforms"; import { normalizeRemoteUrl } from "./common"; import type { AdapterContext, PlatformAdapter, PlatformPublishResult } from "./types"; type ZhihuMeResponse = { uid?: string; id?: string; name?: string; avatar_url?: string; }; type ZhihuImageTokenResponse = { upload_file?: { state?: number; object_key?: string; }; upload_token?: { access_id?: string; access_key?: string; access_token?: string; }; }; type ZhihuDraftCreateResponse = { id?: string; }; type ZhihuPublishResponse = { code?: number; }; function buildHeaders(headers?: HeadersInit): Headers { const next = new Headers(headers); if (!next.has("x-requested-with")) { next.set("x-requested-with", "fetch"); } return next; } async function getCookie(name?: string): Promise { const cookies = await browser.cookies.getAll({ domain: "zhihu.com" }); if (name) { return cookies.find((item) => item.name === name)?.value ?? ""; } return cookies.map((item) => `${item.name}=${item.value}`).join("; "); } async function readJson(response: Response): Promise { const text = await response.text(); if (!response.ok) { throw new Error(text || `zhihu_request_failed_${response.status}`); } if (!text) { return {} as T; } return JSON.parse(text) as T; } async function zhihuFetch(input: string, init?: RequestInit): Promise { const response = await fetch(input, { ...init, credentials: "include", headers: buildHeaders(init?.headers), }); return readJson(response); } function randomTraceId(): string { const uuid = typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `geo-${Date.now()}-${Math.random().toString(16).slice(2)}`; return `${Date.now()},${uuid}`; } function markdownToHtml(markdown: string): string { return marked.parse(markdown, { async: false, gfm: true, breaks: false }) as string; } function baseContent(article: AdapterContext["article"]): string { const html = article.html_content?.trim(); if (html) { return html; } return markdownToHtml(article.markdown_content ?? ""); } function transformContent(html: string): string { let next = html.trim(); next = next.replace(//gi, ""); next = next.replace(/<(script|style)[^>]*>[\s\S]*?<\/\1>/gi, ""); next = next.replace(/\sstyle="[^"]*"/gi, ""); next = next.replace(/\sdata-(?!draft)[a-z-]+="[^"]*"/gi, ""); next = next.replace(/]*>\s*()\s*<\/figure>/gi, "$1"); next = next.replace(/
/gi, '
');
  next = next.replace(/]+src="[^"]+"[^>]*)>/gi, "
"); return next; } function extractImageSources(html: string): string[] { const sources = new Set(); for (const match of html.matchAll(/]*src="([^"]+)"[^>]*>/gi)) { const src = match[1]?.trim(); if (src) { sources.add(src); } } return [...sources]; } async function md5Hex(buffer: ArrayBuffer): Promise { const md5 = md5Lib as unknown as (value: string | ArrayBuffer | Uint8Array) => string; return md5(buffer); } async function hmacSha1Base64(key: string, message: string): Promise { const encoder = new TextEncoder(); const cryptoKey = await crypto.subtle.importKey( "raw", encoder.encode(key), { name: "HMAC", hash: "SHA-1" }, false, ["sign"], ); const signature = await crypto.subtle.sign("HMAC", cryptoKey, encoder.encode(message)); return btoa(String.fromCharCode(...new Uint8Array(signature))); } function buildPictureUrl(imageHash: string, mimeType: string): string { const extension = mimeType.split("/")[1] || "png"; return `https://picx.zhimg.com/v2-${imageHash}.${extension}`; } async function uploadBinaryImage(sourceUrl: string): Promise { const blob = await fetch(sourceUrl).then((response) => response.blob()).catch(() => null); if (!blob || !blob.type.startsWith("image/")) { return null; } const buffer = await blob.arrayBuffer(); const imageHash = await md5Hex(buffer); const token = await zhihuFetch("https://api.zhihu.com/images", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ image_hash: imageHash, source: "article", }), }); if (token.upload_file?.state === 2 && token.upload_file.object_key && token.upload_token) { const ossDate = new Date().toUTCString(); const ossUserAgent = "aliyun-sdk-js/6.8.0 Chrome 139.0.0.0 on OS X 10.15.7 64-bit"; const stringToSign = `PUT\n\n${blob.type}\n${ossDate}\nx-oss-date:${ossDate}\nx-oss-security-token:${token.upload_token.access_token}\nx-oss-user-agent:${ossUserAgent}\n/zhihu-pics/v2-${imageHash}`; const signature = await hmacSha1Base64(token.upload_token.access_key || "", stringToSign); const uploadResponse = await fetch(`https://zhihu-pics-upload.zhimg.com/${token.upload_file.object_key}`, { method: "PUT", headers: { "content-type": blob.type, "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "x-oss-date": ossDate, "x-oss-user-agent": ossUserAgent, "x-oss-security-token": token.upload_token.access_token || "", authorization: `OSS ${token.upload_token.access_id}:${signature}`, }, body: blob, }); if (!uploadResponse.ok) { throw new Error(`zhihu_oss_upload_failed_${uploadResponse.status}`); } } return buildPictureUrl(imageHash, blob.type); } async function processContentImages(html: string): Promise { const sources = extractImageSources(html); if (!sources.length) { return html; } let next = html; const uploaded = new Map(); for (const source of sources) { if (/zhimg\.com/i.test(source)) { continue; } const target = await uploadBinaryImage(source); if (target) { uploaded.set(source, target); } } for (const [from, to] of uploaded.entries()) { next = next.split(from).join(to); } return next; } async function detectZhihu(platform: StoredPlatformState): Promise { try { const me = await zhihuFetch("https://www.zhihu.com/api/v4/me", { method: "GET", }); const uid = me.uid || me.id ? String(me.uid || me.id) : undefined; if (!uid || !me.name) { return { ...platform, connected: false, platform_uid: null, nickname: null, avatar_url: null, message: "未检测到知乎登录态", }; } return { ...platform, connected: true, platform_uid: uid, nickname: me.name, avatar_url: normalizeRemoteUrl(me.avatar_url), message: null, }; } catch (error) { return { ...platform, connected: false, platform_uid: null, nickname: null, avatar_url: null, message: error instanceof Error ? error.message : "知乎登录检测失败", }; } } async function createDraft(context: AdapterContext, titleImage: string | null): Promise { const title = context.article.title?.trim() || "未命名文章"; const body = { delta_time: 0, can_reward: false, title, content: await processContentImages(transformContent(baseContent(context.article))), ...(titleImage ? { titleImage } : {}), }; const created = await zhihuFetch("https://zhuanlan.zhihu.com/api/articles/drafts", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(body), }); if (!created.id) { throw new Error("zhihu_create_draft_failed"); } return created.id; } async function publishDraft(draftId: string): Promise { const xsrfToken = await getCookie("_xsrf"); const result = await zhihuFetch("https://www.zhihu.com/api/v4/content/publish", { method: "POST", headers: { "content-type": "application/json", "x-xsrftoken": xsrfToken, }, body: JSON.stringify({ action: "article", data: { publish: { traceId: randomTraceId(), }, draft: { disabled: 1, id: draftId, isPublished: false, }, commentsPermission: { comment_permission: "anyone", }, creationStatement: { disclaimer_type: "ai_creation", disclaimer_status: "open", }, contentsTables: { table_of_contents_enabled: false, }, commercialReportInfo: { isReport: 0, }, appreciate: { can_reward: false, tagline: "", }, hybridInfo: {}, }, }), }); return result.code === 0; } async function publishZhihu(context: AdapterContext): Promise { const freshState = await detectZhihu({ platform_id: "zhihu", platform_name: "知乎", short_name: "知", accent_color: "#1677ff", login_url: "https://www.zhihu.com/signin", logo_path: "logos/logo_zhihu.png", connected: false, platform_uid: null, nickname: null, avatar_url: null, message: null, }); if (!freshState.connected) { throw new Error("zhihu_not_logged_in"); } const coverUrl = context.article.cover_asset_url?.trim() || null; const titleImage = coverUrl ? await uploadBinaryImage(coverUrl) : null; const draftId = await createDraft(context, titleImage); if (context.article.publish_type === "draft") { return { success: true, status: "pending_review", externalArticleId: draftId, externalArticleUrl: `https://zhuanlan.zhihu.com/p/${draftId}/edit`, externalManageUrl: `https://zhuanlan.zhihu.com/p/${draftId}/edit`, message: "知乎草稿创建成功。", responsePayload: { mode: "zhihu-draft", }, }; } const published = await publishDraft(draftId); if (!published) { throw new Error("zhihu_publish_failed"); } const publishedUrl = `https://zhuanlan.zhihu.com/p/${draftId}`; return { success: true, status: "success", externalArticleId: draftId, externalArticleUrl: publishedUrl, externalManageUrl: `${publishedUrl}/edit`, message: "知乎正式发布成功。", responsePayload: { mode: "zhihu-direct-publish", draft_id: draftId, }, }; } export const zhihuAdapter: PlatformAdapter = { platformId: "zhihu", detect: detectZhihu, publish: publishZhihu, };