【转载】[开源]Exa-Pool:一个优雅的exa搜索api号池,从此搜索api自由!

本文为转载内容,保留原帖观点与结构;如有侵权请联系我处理。

闲来无事整了一个基于Cloudflare Workers的Exa API密钥号池,支持多密钥轮询、自动故障转移和可视化管理面板,从此实现exa自由
:laughing:
基于cloudflare worker+D1数据库,全程无需服务器!

功能特性

  • 密钥轮询 – Round-robin 策略自动分配请求到不同 API 密钥
  • 自动故障转移 – 密钥余额耗尽或失效时自动切换到下一个可用密钥
  • 智能重试 – 请求失败自动重试(最多 3 次)
  • 密钥状态管理 – 自动标记耗尽/失效密钥,支持批量验证
  • 访问控制 – 通过 Allowed Keys 控制谁可以使用代理服务
  • 可视化面板 – Web 管理界面,实时查看密钥状态和请求统计
  • 完整 API 兼容 – 兼容 Exa 官方 API

技术栈

  • Cloudflare Workers
  • Cloudflare D1 (SQLite)
  • Vanilla JavaScript (管理面板)

建议搭配食用 Exa搜索API白嫖60$额度

放个demo站出来,加了一些key,佬们随便刷

:rofl:
与exa官方调用格式完全一样 https://exapool.chengtx.me 调用apikey:linuxdo@chengtx


📌 转载信息
原作者: chengtx
转载时间: 2025/12/10 16:20:19

【转载】分享一个自用小脚本:linux.do 主楼一键收藏到 WordPress

本文为转载内容,保留原帖观点与结构;如有侵权请联系我处理。

最近逛 linux.do 经常看到好帖,想顺手保存到自己 WordPress 里做个人笔记/收藏,所以写了个浏览器脚本分享一下。

说明:

  • 脚本只用于个人收藏/学习记录,不做商业用途。
  • 转载到 WP 后会在文末自动标注原帖链接原作者,版权归原作者所有。
  • 如原作者不希望被转载,请留言,我会及时删除。

功能:

  • 只转载楼主主楼内容(不含评论)
  • 标题加【转载】
  • 修复懒加载图片
  • 可选把外链图片下载到 WP 媒体库
  • 可选草稿/直接发布
  • 文末自动追加转载信息

使用说明:

  1. 电脑浏览器安装 Tampermonkey(油猴)。
  2. 新建脚本,把代码全部粘进去保存。
  3. 修改脚本顶部两处:
  • WP_BASE = "你的博客地址"
  • @connect 你的博客域名
  1. 先在同一浏览器登录一次 WordPress 后台(否则无法发布)。
  2. 打开任意 linux.do 帖子页。
  3. 右下角会出现 “转载到 WP” 按钮。
  4. 点按钮 → 选择分类/是否图片入库/草稿或发布 → 发送即可。

iOS:Safari 装 Userscripts 扩展或用 Orion 浏览器,导入脚本后步骤一样。

下面是正式版代码(已可用):

// ==UserScript==
// @name         linux.do → WordPress 纯转载 正式版(两步发布防超时+无AI+无快捷键+只主楼+图片入库限并发+去图片名+正文优化无目录+Gutenberg无空行+日志框)
// @namespace    http://tampermonkey.net/
// @version      7.0.0
// @description  只转载 linux.do 主楼到 WordPress:无 AI、无快捷键、标题前加【转载】;清理图片文件名/哈希/尺寸/KB/截图软件名等元信息(仅清文本不伤图片);修复懒加载图片;正文结构轻优化(段落/列表/图片/间距,无目录);可选分类/草稿或发布/图片入库;末尾美化转载信息且不被主题打乱;UI 内置运行日志框;图片入库支持并发限速+跳过大图+失败不中断;发布采用“两步发稿”防 WP 超时;intro 为 Gutenberg 段落块,后台编辑器无首行空段落。
// @match        https://linux.do/t/*
// @grant        GM_xmlhttpRequest
// @connect      blog.qinnian.xyz
// ==/UserScript==
 
(function () {
  "use strict";
 
  /************** 配置区(必改) **************/
  const WP_BASE = "https://blog.qinnian.xyz"; // 你的 WordPress(不要以 / 结尾)
  const DEFAULT_STATUS = "draft";             // draft 草稿 / publish 发布
  const DEFAULT_DOWNLOAD_IMAGES = true;       // 默认勾选“图片入库”
  /*******************************************/
 
  const WP_POSTS = `${WP_BASE}/wp-json/wp/v2/posts`;
  const WP_CATEGORIES = `${WP_BASE}/wp-json/wp/v2/categories?per_page=100`;
  const WP_MEDIA = `${WP_BASE}/wp-json/wp/v2/media`;
  const WP_ADMIN = `${WP_BASE}/wp-admin/`;
 
  let cachedNonce = null;
  let cachedNonceAt = 0;
 
  const $ = (sel, root = document) => root.querySelector(sel);
 
  function escapeHtml(str = "") {
    return str.replace(/[&<>"']/g, (ch) => ({
      "&": "&amp;", "<": "&lt;", ">": "&gt;",
      '"': "&quot;", "'": "&#39;",
    }[ch]));
  }
 
  function toErrorMessage(e) {
    if (!e) return "未知错误";
    if (typeof e === "string") return e;
    if (e.message) return e.message;
    try { return JSON.stringify(e); } catch { return String(e); }
  }
 
  // ✅ 防卡死版 GM 请求(强制超时)
  function gmRequest({ method, url, headers = {}, data = null, timeout = 60000, binary = false }) {
    return new Promise((resolve, reject) => {
      let finished = false;
 
      const timer = setTimeout(() => {
        if (finished) return;
        finished = true;
        reject(new Error(`请求超时(>${timeout/1000}s):${url}`));
      }, timeout + 2000);
 
      GM_xmlhttpRequest({
        method, url, headers, data, timeout, binary,
        anonymous: false,
        onload: (resp) => {
          if (finished) return;
          finished = true;
          clearTimeout(timer);
          resolve(resp);
        },
        onerror: (err) => {
          if (finished) return;
          finished = true;
          clearTimeout(timer);
          reject(new Error("网络错误:" + toErrorMessage(err)));
        },
        ontimeout: () => {
          if (finished) return;
          finished = true;
          clearTimeout(timer);
          reject(new Error("GM 请求超时"));
        },
      });
    });
  }
 
  /* ---------------- 0) 获取 WP REST Nonce(基于登录态) ---------------- */
 
  async function getRestNonce() {
    const now = Date.now();
    if (cachedNonce && now - cachedNonceAt < 10 * 60 * 1000) return cachedNonce;
 
    const resp = await gmRequest({ method: "GET", url: WP_ADMIN, timeout: 60000 });
    if (resp.status < 200 || resp.status >= 300) {
      throw new Error(`获取后台页面失败(${resp.status}),请确认已在本浏览器登录 WP 后台。`);
    }
 
    const html = resp.responseText || "";
    const m1 = html.match(/wpApiSettings\s*=\s*{[^}]*"nonce"\s*:\s*"([^"]+)"/);
    const m2 = html.match(/"rest_nonce"\s*:\s*"([^"]+)"/);
    const nonce = (m1 && m1[1]) || (m2 && m2[1]);
 
    if (!nonce) {
      throw new Error("未能解析 REST Nonce,请检查是否有安全插件移除了后台 nonce。");
    }
 
    cachedNonce = nonce;
    cachedNonceAt = now;
    return nonce;
  }
 
  /* ---------------- 1) 提取 linux.do 主楼 + 只清文本不伤图 ---------------- */
 
  function extractLinuxDoOP() {
    const rawTitle =
      $("#topic-title h1 a, #topic-title a")?.innerText?.trim() ||
      document.title.trim();
 
    const title = `【转载】${rawTitle}`;
 
    const firstPost = document.querySelector(".post-stream article[data-post-id]");
    if (!firstPost) throw new Error("未找到主楼(第一个帖子元素不存在)");
 
    const author =
      firstPost.dataset.username ||
      firstPost.querySelector(".username")?.innerText?.trim() ||
      "";
 
    const cooked = firstPost.querySelector(".cooked");
    if (!cooked) throw new Error("未找到主楼正文(.cooked)");
 
    const clone = cooked.cloneNode(true);
 
    clone.querySelectorAll(
      "script, style, nav, footer, .quote-controls, .post-menu-area, .onebox-metadata"
    ).forEach(n => n.remove());
 
    clone.querySelectorAll("figcaption").forEach(n => n.remove());
 
    // 清理 a(img) 周围的文本
    clone.querySelectorAll("a").forEach(a => {
      if (a.querySelector("img")) {
        [...a.childNodes].forEach(node => {
          if (node.nodeType === Node.TEXT_NODE && node.textContent.trim()) node.remove();
        });
      }
    });
 
    // 修复懒加载
    clone.querySelectorAll("img").forEach(img => {
      const src = img.getAttribute("src") || "";
      const dataSrc =
        img.getAttribute("data-src") ||
        img.getAttribute("data-original") ||
        img.getAttribute("data-lazy-src") ||
        img.getAttribute("data-actualsrc") ||
        "";
      if ((!src || src === "about:blank") && dataSrc) {
        img.setAttribute("src", dataSrc);
      }
    });
 
    // 判断是否是“图片名/尺寸/哈希/KB/截图软件名”等元文本
    const isMetaText = (t = "") => {
      const s = t.trim();
      if (!s) return false;
 
      const hasSize = /\d+\s*[x×]\s*\d+/.test(s);
      const hasWeight = /\d+(\.\d+)?\s*(kb|mb)\b/i.test(s);
 
      const cleaned = s.replace(/\s+/g, "");
      const hexish = cleaned.match(/[0-9A-Fa-f-]/g) || [];
      const ratio = hexish.length / cleaned.length;
      const looksHash = cleaned.length >= 8 && ratio > 0.9;
 
      const shotName =
        /^(pixpin|snipaste|screenshot|screen_shot|img|wechat|wx_camera|photo|image|截图|截屏)[-_ ]*/i.test(s);
 
      return hasSize || hasWeight || looksHash || shotName;
    };
 
    // 清文字节点
    const walker = document.createTreeWalker(clone, NodeFilter.SHOW_TEXT, null);
    const textNodes = [];
    while (walker.nextNode()) textNodes.push(walker.currentNode);
    textNodes.forEach(tn => { if (isMetaText(tn.textContent)) tn.remove(); });
 
    // 清容器式元文本
    clone.querySelectorAll("p, div, span, li, code, em, strong").forEach(el => {
      const txt = el.textContent.trim();
      if (isMetaText(txt) && el.querySelectorAll("img").length === 0) el.remove();
    });
 
    let html = clone.innerHTML.trim();
    html = html.replace(/<p>\s*<\/p>/gi, "");
    html = html.replace(/<div>\s*<\/div>/gi, "");
 
    return {
      title,
      content: html,
      author_name: author,
      source_url: location.href.split("?")[0],
    };
  }
 
  /* ---------------- 1.5) 正文结构轻优化(无目录) ---------------- */
 
  function optimizeBodyHTML(html) {
    if (!html) return html;
 
    html = html
      .replace(/\r\n/g, "\n")
      .replace(/<br\s*\/?>\s*<br\s*\/?>/gi, "\n\n")
      .replace(/<br\s*\/?>/gi, "\n");
 
    const doc = new DOMParser().parseFromString(`<div id="root">${html}</div>`, "text/html");
    const root = doc.querySelector("#root");
 
    const textWalker = doc.createTreeWalker(root, NodeFilter.SHOW_TEXT, null);
    const textNodes = [];
    while (textWalker.nextNode()) textNodes.push(textWalker.currentNode);
 
    textNodes.forEach(tn => {
      const text = tn.textContent;
      if (!text || !text.trim()) return;
 
      const ptag = tn.parentElement?.tagName?.toLowerCase();
      if (["code","pre","blockquote","li"].includes(ptag)) return;
 
      const blocks = text.split(/\n{2,}/).map(b => b.trim()).filter(Boolean);
      if (blocks.length <= 1) return;
 
      const frag = doc.createDocumentFragment();
 
      blocks.forEach(block => {
        const lines = block.split("\n").map(l => l.trim()).filter(Boolean);
 
        const isOL = lines.length > 1 && lines.every(l => /^\d+[\.\)]\s+/.test(l));
        if (isOL) {
          const ol = doc.createElement("ol");
          lines.forEach(l => {
            const li = doc.createElement("li");
            li.textContent = l.replace(/^\d+[\.\)]\s+/, "");
            ol.appendChild(li);
          });
          frag.appendChild(ol);
          return;
        }
 
        const isUL = lines.length > 1 && lines.every(l => /^[-•*]\s+/.test(l));
        if (isUL) {
          const ul = doc.createElement("ul");
          lines.forEach(l => {
            const li = doc.createElement("li");
            li.textContent = l.replace(/^[-•*]\s+/, "");
            ul.appendChild(li);
          });
          frag.appendChild(ul);
          return;
        }
 
        const p = doc.createElement("p");
        p.textContent = block;
        frag.appendChild(p);
      });
 
      tn.parentNode.replaceChild(frag, tn);
    });
 
    // 图片居中 + 自适应
    root.querySelectorAll("img").forEach(img => {
      const parent = img.parentElement;
      if (parent && parent.tagName.toLowerCase() === "figure") return;
 
      const fig = doc.createElement("figure");
      fig.style.cssText = "margin:16px auto;text-align:center;";
      img.style.cssText = (img.getAttribute("style") || "") + ";max-width:100%;height:auto;border-radius:6px;";
 
      parent.insertBefore(fig, img);
      fig.appendChild(img);
    });
 
    root.querySelectorAll("p").forEach(p => {
      p.style.cssText = (p.getAttribute("style") || "") + ";margin:0 0 14px;line-height:1.8;font-size:16px;";
    });
    root.querySelectorAll("ul,ol").forEach(list => {
      list.style.cssText = (list.getAttribute("style") || "") + ";margin:0 0 14px 20px;line-height:1.8;font-size:16px;";
    });
    root.querySelectorAll("li").forEach(li => {
      li.style.cssText = (li.getAttribute("style") || "") + ";margin:4px 0;";
    });
 
    // 去掉空容器
    root.querySelectorAll("p,div,span").forEach(el => {
      if (!el.textContent.trim() && el.querySelectorAll("img,video,pre,code,blockquote,ul,ol").length === 0) {
        el.remove();
      }
    });
 
    return root.innerHTML.trim();
  }
 
  /* ---------------- 2) 拉分类 ---------------- */
 
  async function fetchCategories() {
    try {
      const resp = await gmRequest({
        method: "GET",
        url: WP_CATEGORIES,
        headers: { "Content-Type": "application/json" }
      });
      if (resp.status >= 200 && resp.status < 300) {
        const arr = JSON.parse(resp.responseText || "[]");
        return arr.map(c => ({ id: c.id, name: c.name }));
      }
    } catch (e) {
      console.warn("拉取分类失败:", e);
    }
    return [];
  }
 
  /* ---------------- 3) 图片入库相关(限并发 + 大图跳过) ---------------- */
 
  async function downloadImageAsArrayBuffer(url) {
    const resp = await new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        method: "GET",
        url,
        responseType: "arraybuffer",
        anonymous: true,
        onload: r => resolve(r),
        onerror: e => reject(new Error("图片下载失败:" + toErrorMessage(e))),
        ontimeout: () => reject(new Error("图片下载超时")),
      });
    });
 
    if (resp.status < 200 || resp.status >= 300) {
      throw new Error(`图片下载失败(${resp.status})`);
    }
    return resp;
  }
 
  async function uploadImageToMedia(url, nonce) {
    const resp = await downloadImageAsArrayBuffer(url);
    const arrayBuffer = resp.response;
 
    const headers = resp.responseHeaders || "";
    const m = headers.match(/content-type:\s*([^\r\n]+)/i);
    const contentType = m ? m[1].trim() : "image/jpeg";
 
    const urlObj = new URL(url);
    let filename = urlObj.pathname.split("/").pop() || "image";
    if (!/\.(png|jpe?g|gif|webp|svg)$/i.test(filename)) {
      if (/png/i.test(contentType)) filename += ".png";
      else if (/webp/i.test(contentType)) filename += ".webp";
      else if (/gif/i.test(contentType)) filename += ".gif";
      else if (/svg/i.test(contentType)) filename += ".svg";
      else filename += ".jpg";
    }
 
    const uploadResp = await new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        method: "POST",
        url: WP_MEDIA,
        headers: {
          "Content-Type": contentType,
          "Content-Disposition": `attachment; filename="${filename}"`,
          "X-WP-Nonce": nonce,
        },
        data: arrayBuffer,
        binary: true,
        anonymous: false,
        onload: r => resolve(r),
        onerror: e => reject(new Error("图片上传失败:" + toErrorMessage(e))),
        ontimeout: () => reject(new Error("图片上传超时")),
      });
    });
 
    if (uploadResp.status < 200 || uploadResp.status >= 300) {
      throw new Error(`图片上传失败(${uploadResp.status}): ${uploadResp.responseText}`);
    }
 
    const json = JSON.parse(uploadResp.responseText || "{}");
    return json.source_url || "";
  }
 
  async function mapWithConcurrency(items, limit, mapper) {
    const results = new Array(items.length);
    let idx = 0;
 
    async function worker() {
      while (idx < items.length) {
        const current = idx++;
        try {
          results[current] = await mapper(items[current], current);
        } catch (e) {
          results[current] = { __error: e };
        }
      }
    }
 
    const workers = Array.from({ length: limit }, () => worker());
    await Promise.all(workers);
    return results;
  }
 
  async function downloadAndReplaceImages(post, nonce, log = null) {
    const doc = new DOMParser().parseFromString(post.content, "text/html");
    const imgs = Array.from(doc.querySelectorAll("img"));
    if (!imgs.length) return post;
 
    const wpHost = new URL(WP_BASE).host;
    const base = post.source_url || location.href;
    const cacheMap = {};
 
    const CONCURRENCY = 3;
    const MAX_SIZE_MB = 8;
 
    const tasks = imgs.map(img => {
      const src = img.getAttribute("src");
      if (!src) return null;
 
      let abs;
      try { abs = new URL(src, base).href; }
      catch { return null; }
 
      const host = new URL(abs).host;
      if (host === wpHost) return null;
 
      return { img, abs };
    }).filter(Boolean);
 
    if (log) log(`发现外链图片 ${tasks.length} 张,开始处理(并发=${CONCURRENCY})`);
 
    await mapWithConcurrency(tasks, CONCURRENCY, async (t, i) => {
      const { img, abs } = t;
 
      if (cacheMap[abs]) {
        img.setAttribute("src", cacheMap[abs]);
        if (log) log(`[#${i+1}] 使用缓存:${abs}`);
        return;
      }
 
      try {
        if (log) log(`[#${i+1}] 下载图片:${abs}`);
        const resp = await downloadImageAsArrayBuffer(abs);
 
        let sizeBytes = 0;
        const h = resp.responseHeaders || "";
        const m = h.match(/content-length:\s*(\d+)/i);
        if (m) sizeBytes = parseInt(m[1], 10) || 0;
        else sizeBytes = resp.response?.byteLength || 0;
 
        const sizeMB = sizeBytes / (1024 * 1024);
 
        if (sizeMB > MAX_SIZE_MB) {
          if (log) log(`[#${i+1}] 图片过大(${sizeMB.toFixed(1)}MB) 跳过入库`);
          return;
        }
 
        if (log) log(`[#${i+1}] 上传媒体库(${sizeMB.toFixed(1)}MB)`);
        const newUrl = await uploadImageToMedia(abs, nonce);
 
        if (newUrl) {
          cacheMap[abs] = newUrl;
          img.setAttribute("src", newUrl);
          if (log) log(`[#${i+1}] 入库成功 → ${newUrl}`);
        } else {
          if (log) log(`[#${i+1}] 入库失败(空返回)`);
        }
      } catch (e) {
        if (log) log(`[#${i+1}] 入库异常:${toErrorMessage(e)},保留外链`);
      }
    });
 
    if (log) log("图片处理完成");
    return { ...post, content: doc.body.innerHTML };
  }
 
  /* ---------------- 4) 两步发布(防超时) ---------------- */
 
  async function updatePostContent(postId, content, nonce, log) {
    const resp = await gmRequest({
      method: "POST",
      url: `${WP_POSTS}/${postId}`,
      headers: {
        "Content-Type": "application/json",
        "X-WP-Nonce": nonce,
      },
      data: JSON.stringify({ content }),
      timeout: 90000,
    });
 
    if (resp.status >= 200 && resp.status < 300) {
      if (log) log("二次更新正文成功");
      return JSON.parse(resp.responseText || "{}");
    }
    throw new Error(`二次更新失败(${resp.status}): ${resp.responseText}`);
  }
 
  async function updatePostStatus(postId, status, nonce, log) {
    const resp = await gmRequest({
      method: "POST",
      url: `${WP_POSTS}/${postId}`,
      headers: {
        "Content-Type": "application/json",
        "X-WP-Nonce": nonce,
      },
      data: JSON.stringify({ status }),
      timeout: 60000,
    });
 
    if (resp.status >= 200 && resp.status < 300) {
      if (log) log(`状态更新成功 → ${status}`);
      return JSON.parse(resp.responseText || "{}");
    }
    throw new Error(`状态更新失败(${resp.status}): ${resp.responseText}`);
  }
 
  async function sendPostToWordPress(post, { categoryId, status, nonce, log }) {
    const now = new Date().toLocaleString("zh-CN", { hour12: false });
 
    // ✅ intro:Gutenberg 段落块开头,后台无首行空段落
    const intro =
`<!-- wp:paragraph -->
<p style="
  margin:0 0 10px 0;
  padding:7px 10px;
  background:#f6f7f9;
  border-left:3px solid #94a3b8;
  border-radius:6px;
  font-size:13.5px;
  color:#475569;
  line-height:1.7;
">
  本文为转载内容,保留原帖观点与结构;如有侵权请联系我处理。
</p>
<!-- /wp:paragraph -->`;
 
    // ✅ footer:div + span inline,主题不会乱
    const footer =
`<div style="margin-top:18px;">
  <hr style="border:0;height:1px;background:#e2e8f0;margin:16px 0;">
 
  <div style="
    padding:12px 14px;
    background:#f8fafc;
    border:1px dashed #d0d7de;
    border-radius:12px;
    font-size:14px;
    color:#334155;
    line-height:1.8;
  ">
    <div style="margin:0 0 8px 0;font-weight:700;color:#0f172a;font-size:15px;">
      📌 转载信息
    </div>
 
    <div style="margin:0 0 6px 0;">
      <span style="color:#64748b;display:inline;font-weight:600;">来源:</span>
      <a href="${post.source_url}" target="_blank" rel="nofollow noopener"
         style="display:inline;color:#2563eb;text-decoration:underline;word-break:break-all;">
        ${post.source_url}
      </a>
    </div>
 
    ${post.author_name ? `
    <div style="margin:0 0 6px 0;">
      <span style="color:#64748b;display:inline;font-weight:600;">原作者:</span>
      <span style="display:inline;font-weight:600;">${escapeHtml(post.author_name)}</span>
    </div>` : ""}
 
    <div style="margin:0;">
      <span style="color:#64748b;display:inline;font-weight:600;">转载时间:</span>
      <span style="display:inline;">${now}</span>
    </div>
  </div>
</div>`;
 
    let optimizedBody = optimizeBodyHTML(post.content).trim();
 
    // ✅ 彻底清掉正文开头空白/空段落/&nbsp;/br
    optimizedBody = optimizedBody.replace(
      /^(\s|&nbsp;|<br\s*\/?>|<p>\s*(?:&nbsp;|\u00a0|<br\s*\/?>|\s)*<\/p>)+/i,
      ""
    );
 
    const finalContent = intro.trim() + optimizedBody + footer.trim();
 
    // ✅ 第一步:占位草稿(秒返回)
    const firstPayload = {
      title: post.title,
      content: "<p>草稿占位,正在写入正文...</p>",
      status: "draft",
      categories: categoryId > 0 ? [categoryId] : [],
    };
 
    if (log) log("第1步:创建占位草稿...");
    const r1 = await gmRequest({
      method: "POST",
      url: WP_POSTS,
      headers: {
        "Content-Type": "application/json",
        "X-WP-Nonce": nonce,
      },
      data: JSON.stringify(firstPayload),
      timeout: 60000,
    });
 
    if (r1.status < 200 || r1.status >= 300) {
      throw new Error(`创建草稿失败(${r1.status}): ${r1.responseText}`);
    }
 
    const created = JSON.parse(r1.responseText || "{}");
    const postId = created.id;
    if (log) log(`草稿创建成功,ID=${postId}`);
 
    // ✅ 第二步:写入完整正文
    if (log) log("第2步:写入完整正文...");
    let updated = await updatePostContent(postId, finalContent, nonce, log);
 
    // ✅ 第三步:切换 publish(可选)
    if (status === "publish") {
      if (log) log("第3步:切换为 publish...");
      updated = await updatePostStatus(postId, "publish", nonce, log);
    }
 
    return updated;
  }
 
  /* ---------------- 5) UI 弹窗 + 日志框 ---------------- */
 
  function openRepostUI() {
    let postData = null;
    let categories = [];
 
    const mask = document.createElement("div");
    mask.style.cssText = `
      position:fixed; inset:0; background:rgba(0,0,0,.45);
      z-index:999999; display:flex; align-items:center; justify-content:center;
      font-family: system-ui, -apple-system, Segoe UI, Roboto, PingFang SC, sans-serif;
    `;
 
    const modal = document.createElement("div");
    modal.style.cssText = `
      width:520px; max-width:90vw; background:#fff; border-radius:14px;
      box-shadow:0 12px 40px rgba(0,0,0,.26); padding:18px 18px 14px;
    `;
 
    modal.innerHTML = `
      <div style="font-size:18px;font-weight:700;margin-bottom:6px;">
        转载到 WordPress(只主楼)
      </div>
      <div id="dw-title" style="font-size:13px;color:#666;margin-bottom:8px;max-height:48px;overflow:hidden;"></div>
 
      <label style="display:block;font-size:14px;margin:6px 0 4px;">选择分类</label>
      <select id="dw-cat" style="width:100%;padding:8px;border:1px solid #ddd;border-radius:8px;font-size:14px;">
        <option value="0">不设置分类</option>
      </select>
 
      <div style="display:flex;gap:12px;margin-top:10px;flex-wrap:wrap;">
        <label style="display:flex;align-items:center;gap:6px;font-size:14px;">
          <input id="dw-img" type="checkbox" ${DEFAULT_DOWNLOAD_IMAGES ? "checked" : ""} />
          图片入库(下载到媒体库)
        </label>
 
        <label style="display:flex;align-items:center;gap:6px;font-size:14px;">
          <input id="dw-pub" type="checkbox" ${DEFAULT_STATUS === "publish" ? "checked" : ""} />
          直接发布(否则草稿)
        </label>
      </div>
 
      <div id="dw-status" style="margin-top:8px;font-size:13px;color:#888;"></div>
 
      <div style="margin-top:10px;">
        <div style="font-size:13px;color:#666;margin-bottom:6px;">运行日志</div>
        <div id="dw-log" style="
          height:140px;
          overflow:auto;
          background:#0b1020;
          color:#d1d5db;
          font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
          font-size:12px;
          line-height:1.6;
          padding:8px 10px;
          border-radius:8px;
          border:1px solid #111827;
          white-space:pre-wrap;
        "></div>
      </div>
 
      <div style="display:flex;justify-content:flex-end;gap:8px;margin-top:14px;">
        <button id="dw-cancel" style="padding:8px 12px;border-radius:8px;border:1px solid #ddd;background:#fff;cursor:pointer;">
          取消
        </button>
        <button id="dw-send" style="padding:8px 14px;border-radius:8px;border:none;background:#111;color:#fff;cursor:pointer;">
          发送
        </button>
      </div>
    `;
 
    mask.appendChild(modal);
    document.body.appendChild(mask);
 
    const q = (sel) => modal.querySelector(sel);
    const statusEl = q("#dw-status");
    const sendBtn = q("#dw-send");
    const logEl = q("#dw-log");
 
    function log(msg) {
      const t = new Date().toLocaleTimeString("zh-CN", { hour12:false });
      logEl.textContent += `[${t}] ${msg}\n`;
      logEl.scrollTop = logEl.scrollHeight;
    }
 
    q("#dw-cancel").onclick = () => mask.remove();
 
    (async () => {
      try {
        log("开始提取主楼内容...");
        postData = extractLinuxDoOP();
        q("#dw-title").textContent = "标题:" + postData.title;
        log("提取成功");
 
        log("拉取 WordPress 分类...");
        categories = await fetchCategories();
        const catSel = q("#dw-cat");
        categories.forEach((c) => {
          const opt = document.createElement("option");
          opt.value = c.id;
          opt.textContent = c.name;
          catSel.appendChild(opt);
        });
        log(`分类数量:${categories.length}`);
 
        statusEl.textContent = "准备就绪,选择分类/选项后点击发送。";
        log("准备就绪");
      } catch (e) {
        const em = toErrorMessage(e);
        statusEl.textContent = "❌ 提取帖子失败:" + em;
        log("提取帖子失败:" + em);
        sendBtn.disabled = true;
      }
    })();
 
    sendBtn.onclick = async () => {
      try {
        sendBtn.disabled = true;
 
        const categoryId = parseInt(q("#dw-cat").value, 10) || 0;
        const downloadImages = q("#dw-img").checked;
        const status = q("#dw-pub").checked ? "publish" : "draft";
 
        statusEl.textContent = "检查 WordPress 登录态...";
        log("检查 WordPress 登录态...");
        const nonce = await getRestNonce();
        log("Nonce 获取成功");
 
        if (downloadImages) {
          statusEl.textContent = "正在下载并将图片上传到媒体库...";
          log("开始图片入库与替换外链...");
          postData = await downloadAndReplaceImages(postData, nonce, log);
          log("图片入库流程结束");
        } else {
          log("图片入库未开启,跳过");
        }
 
        statusEl.textContent = "正在发布到 WordPress...";
        log(`开始发布(status=${status}, categoryId=${categoryId || 0})`);
 
        const res = await sendPostToWordPress(postData, { categoryId, status, nonce, log });
 
        const postId = res.id || res.ID;
        const link = res.link || (res.guid && res.guid.rendered) || "#";
 
        statusEl.innerHTML = `✅ 发布成功!文章ID: ${postId}<br><a href="${link}" target="_blank">打开文章</a>`;
        log(`发布成功:ID=${postId}`);
        log(`文章链接:${link}`);
      } catch (e) {
        const em = toErrorMessage(e);
        statusEl.textContent = "❌ 发布失败:" + em;
        log("发布失败:" + em);
      } finally {
        sendBtn.disabled = false;
      }
    };
  }
 
  /* ---------------- 6) 右下角按钮 ---------------- */
 
  function initButton() {
    const btn = document.createElement("button");
    btn.textContent = "转载到 WP";
    btn.style.cssText = `
      position:fixed; right:20px; bottom:20px; z-index:99999;
      padding:8px 14px; border:none; border-radius:9px;
      background:#111; color:#fff; cursor:pointer; font-size:14px;
      box-shadow:0 6px 20px rgba(0,0,0,.25);
    `;
    btn.addEventListener("click", openRepostUI);
    document.body.appendChild(btn);
  }
 
  initButton();
})();


我自己的博客是 [https://blog.qinnian.xyz

,平时会把看到的好内容做整理/笔记,也会同步一些工具和折腾记录。]

如果有佬友也在写博客、愿意互相交流的话,欢迎来逛逛~

想互挂友情链接的朋友直接回帖/私信我站点地址


📌 转载信息
原作者: qinnian
转载时间: 2025/12/10 16:18:19

我的备忘录