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 wrapStandaloneImages(html: string): string { if (typeof DOMParser === "undefined") { return html.replace(/]+src="[^"]+"[^>]*)>/gi, "
"); } const doc = new DOMParser().parseFromString(`
${html}
`, "text/html"); const root = doc.body.querySelector('[data-root="zhihu-content"]'); if (!(root instanceof HTMLElement)) { return html; } for (const image of [...root.querySelectorAll("img")]) { if (image.closest("figure")) { continue; } const parent = image.parentElement; if (parent?.tagName === "P") { continue; } const figure = doc.createElement("figure"); image.parentNode?.insertBefore(figure, image); figure.appendChild(image); } return root.innerHTML.trim(); } 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 = wrapStandaloneImages(next);

  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: "",
          disclaimer_status: "",
        },
        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,
};