// ==UserScript==
// @name 吾志日记导出器 (Wuzhi.me Exporter)
// @namespace http://tampermonkey.net/
// @version 2.0
// @description 通过日历精确导出吾志 (wuzhi.me) 的所有日记
// @author karate3008
// @match *://wuzhi.me/*
// @match *://www.wuzhi.me/*
// @grant none
// ==/UserScript==
(function () {
'use strict';
// ==========================================
// 状态变量
// ==========================================
let isExporting = false;
let allDiaries = [];
let processedMonths = new Set(); // 避免重复处理月份
let totalDays = 0;
// ==========================================
// 创建悬浮 UI
// ==========================================
// ==========================================
// 初始化逻辑
// ==========================================
function init() {
if (document.getElementById('wz-export-ui')) return; // 防止重复初始化
const ui = document.createElement('div');
ui.id = 'wz-export-ui';
ui.innerHTML = `
⚠️ 提示:请先从日历跳转到有日记的月份,程序将自动向前回溯导出全部数据。
准备就绪,点击开始...
Wecho (回响)
每一缕思绪,都是一个漂流瓶
永久免费 · 端到端加密 · 纯粹记录
`;
document.body.appendChild(ui);
// 获取元素引用
statusDiv = document.getElementById('wz-status');
startBtn = document.getElementById('wz-start-btn');
stopBtn = document.getElementById('wz-stop-btn');
importBtn = document.getElementById('wz-import-btn');
introDiv = document.getElementById('wz-intro');
pMonth = document.getElementById('wz-p-month');
pDays = document.getElementById('wz-p-days');
pCount = document.getElementById('wz-p-count');
pMonths = document.getElementById('wz-p-months');
progressDiv = document.getElementById('wz-progress');
// 绑定事件
startBtn.onclick = startExport;
stopBtn.onclick = stopExport;
importBtn.onclick = () => window.open('https://wecho.me', '_blank');
console.log('[吾志导出] UI 初始化完成');
}
// 全局变量声明 (在 init 中赋值)
let statusDiv, startBtn, stopBtn, importBtn, introDiv, pMonth, pDays, pCount, pMonths, progressDiv;
// ==========================================
// 启动逻辑
// ==========================================
console.log('[吾志导出] 脚本已注入,准备初始化...');
function tryInit() {
// 如果 UI 已存在,不再重复
if (document.getElementById('wz-export-ui')) return;
// 如果 body 还没准备好,延迟重试
if (!document.body) {
console.log('[吾志导出] document.body 未就绪,500ms 后重试...');
setTimeout(tryInit, 500);
return;
}
console.log('[吾志导出] document.body 就绪,执行 init()...');
init();
}
// 尝试策略 1: 立即尝试
tryInit();
// 尝试策略 2: 页面加载完成后
window.addEventListener('load', tryInit);
// 尝试策略 3: DOM 内容加载后
document.addEventListener('DOMContentLoaded', tryInit);
// 尝试策略 4: 如果当前已经是完成状态
if (document.readyState === 'complete' || document.readyState === 'interactive') {
tryInit();
}
// ==========================================
// 工具函数
// ==========================================
function updateStatus(msg) {
statusDiv.innerHTML = msg;
console.log(`[吾志导出] ${msg.replace(/<[^>]*>/g, ' ')}`);
}
function updateProgress(month, days, count, months) {
if (month !== null) pMonth.textContent = month;
if (days !== null) { totalDays += days; pDays.textContent = totalDays; }
pCount.textContent = allDiaries.length;
if (months !== null) pMonths.textContent = months;
}
function sleep(ms) {
return new Promise(r => setTimeout(r, ms));
}
// ==========================================
// 核心逻辑
// ==========================================
async function startExport() {
if (isExporting) return;
isExporting = true;
allDiaries = [];
processedMonths.clear();
totalDays = 0;
startBtn.style.display = 'none';
stopBtn.style.display = 'block';
importBtn.style.display = 'none';
progressDiv.style.display = 'block';
updateStatus('正在获取日历...');
updateProgress('-', null, 0, 0);
try {
// 检查当前页面日历是否有日记日期
const calendarLinks = document.querySelectorAll('.calendar td a[href*="/archive/day/"]');
if (calendarLinks.length === 0) {
updateStatus('⚠️ 当前日历无日记,请先跳转到有日记的月份');
isExporting = false;
startBtn.style.display = 'block';
stopBtn.style.display = 'none';
return;
}
// 从当前页面开始,获取日历
await processCalendarPage(window.location.href);
} catch (e) {
console.error(e);
updateStatus('出错: ' + e.message);
}
if (isExporting) {
stopExport();
}
}
function stopExport() {
isExporting = false;
startBtn.style.display = 'block';
startBtn.textContent = '重新导出';
stopBtn.style.display = 'none';
introDiv.style.display = 'block';
importBtn.style.display = 'block';
updateStatus(`✅ 完成!共 ${allDiaries.length} 篇`);
downloadData();
}
/**
* 处理一个包含日历的页面
* 1. 提取日历中所有有日记的日期链接 (/archive/day/YYYY-MM-DD)
* 2. 逐个获取每天的日记
* 3. 找到上一个月的链接,继续处理
*/
async function processCalendarPage(url) {
if (!isExporting) return;
try {
const response = await fetch(url, { credentials: 'include' });
const text = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(text, 'text/html');
// 提取当前日历的年月标识(用于防止重复处理)
const monthId = extractMonthId(doc);
if (monthId && processedMonths.has(monthId)) {
console.log(`[吾志导出] 月份 ${monthId} 已处理过,跳过`);
return;
}
if (monthId) {
processedMonths.add(monthId);
}
updateStatus(`正在处理: ${monthId || '当前月'}`);
updateProgress(monthId || '当前月', null, null, processedMonths.size);
// 找到日历中所有有日记的日期
// 格式: