平时逛论坛看到好帖,总想顺手收藏到自己的 WordPress 博客里。手动复制粘贴倒也能做,但遇到长文 + 多图就很折腾:
要找主楼、去掉无关内容、修复懒加载、把外链图片搬到媒体库、最后还要写转载出处……一套流程下来,热情都被磨光了。
所以我干脆写了一个 “浏览器侧一键转载”脚本,在 linux.do 浏览帖子时点一下按钮,就能把主楼内容发到我的 WordPress。
不需要在 WordPress 里装任何插件,整个过程都在我自己的浏览器里完成,适合轻量、可控、随手转载的场景。
脚本能做什么?
一句话:只转载主楼 → 自动优化正文 → 可选图片入库 → 末尾标明转载信息 → 发布到 WP
具体功能:
1) 只转载主楼
- 只抓取楼主(OP)的正文
- 不包含评论、楼层回复
- 避免转载成“评论合集”,更干净
2) 标题自动加前缀
转载后的文章标题会变成:
【转载】原帖标题
便于自己归档,也对读者更直观。
3) 修复论坛常见图片问题
linux.do 的图片经常出现:
- 懒加载
data-src - 复制后变成空白图
- 图旁边还带文件名、尺寸、KB 等“元文本”
脚本会:
- 自动把
data-src转为正常src - 清理图片旁边的哈希/文件名/尺寸/KB/截图软件名等“脏文本”
- 只清文本,不破坏图片标签,阅读更舒服
4) 正文结构简单优化(无目录)
不会瞎改原帖意思,只做“轻排版”:
- 去掉头尾空段落
- 连续换行归并成段落
- 识别简单列表变成 ul/ol
- 图片自动居中、最大宽度自适应
- 不生成目录(你要求的)
5) 可选:外链图片下载到 WP 媒体库
如果你勾选“图片入库”,脚本会:
- 下载外链图片
- 逐个上传到 WordPress 媒体库
- 把正文内图片链接替换为本地媒体库地址
而且考虑到“图多会卡”的情况:
- 并发上传限制为 3(手机更稳)
- 超过 8MB 的大图自动跳过
- 单张失败不中断整篇发布
6) 自动追加美化后的转载信息
文章末尾会自动加一块“转载信息”:
- 来源链接
- 原作者(楼主用户名)
- 转载时间
样式专门调过,兼容主题/编辑器,不乱、不额外空行。
7) 两步发布,避免 REST 超时
有些 WordPress 站点 POST 创建文章会慢/卡/超时(尤其图多、插件多时)。
脚本采用“两步发布”:
- 先创建一个极简草稿占位
- 再把完整正文写进去
- 如果你勾了“直接发布”,再切换为 publish
这样稳定性大幅提升。
使用方法
电脑端(Chrome/Edge/Firefox)
- 安装 Tampermonkey 扩展
- 新建脚本,把代码完整粘进去保存
- 访问任意 linux.do 帖子页
- 右下角会出现 “转载到 WP” 按钮
- 点按钮 → 选择分类/草稿或发布/是否图片入库 → 发送
注意:第一次使用前,请先在同一浏览器里登录 WordPress 后台一次,脚本需要读取 REST nonce。
iOS(手机)使用
iOS 不支持 Tampermonkey,但可以用两种方式跑 Userscript:
推荐:Safari + Userscripts 扩展
- App Store 安装 Userscripts
- 设置 → Safari → 扩展 → Userscripts → 允许所有网站
- 在 Userscripts App 里新建
.user.js文件,粘贴脚本 - Safari 打开 linux.do 帖子
- aA → 扩展 → Userscripts → 启用脚本
- 刷新后右下角按钮出现
备选:Orion 浏览器
Orion 原生支持 Userscript,配置更省一步。
一些常见问题
Q1. 为什么发布会超时?
如果你连“占位草稿”都发不出去,那一般是 WordPress 服务器端的问题:
插件保存钩子太重、内存不足、WAF 静默拦截 REST POST 等。
脚本做了客户端防卡死,但根治还是要调服务器(我后面可能单独写一篇)。
Q2. 手机入库图片会不会很慢?
会慢一点,毕竟手机带宽/内存弱。
你可以临时取消勾选“图片入库”,先发正文再说。
Q3. 会不会转载到错误的楼层?
不会。本脚本仅抓主楼 .post-stream article[data-post-id] 的 first post。
Q4. 转载会不会破坏原帖排版?
只做轻度结构清理和图片居中,不改语义。
你仍然可以在 WP 编辑器里二次微调。
为什么我选择“浏览器侧脚本”而不是 WordPress 插件?
- 不污染 WordPress:不装插件、不改主题、不增加后台负担
- 随用随开:只在自己浏览器运行,想转载就点,不想就关
- 安全可控:完全在个人环境里执行,不交给第三方服务
- 迁移简单:换站点只需改 WP 域名和 connect
最后
这个脚本是我自己日常使用的工具,优先保证“稳定和干净”。
后续我还计划加一些可选功能,比如:
- 自动检测是否重复转载
- 自动设置特色图
- 手机下“最多入库前 N 张图片”的开关
如果你也常看 linux.do / 其他 Discourse 论坛,又想把好内容沉淀到自己的 WordPress,欢迎试试。
需要反馈问题,可以留言交流~
// ==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) => ({
"&": "&", "<": "<", ">": ">",
'"': """, "'": "'",
}[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();
// ✅ 彻底清掉正文开头空白/空段落/ /br
optimizedBody = optimizedBody.replace(
/^(\s| |<br\s*\/?>|<p>\s*(?: |\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();
})();
Comments NOTHING