// ==UserScript==
// @name 繁体转简体(使用繁化姬API)
// @namespace http://tampermonkey.net/
// @version 0.3
// @description 使用繁化姬API自动将网页繁体中文转换为简体中文
// @author Claude 3.7
// @match *://*/*
// @grant GM_xmlhttpRequest
// @connect api.zhconvert.org
// @run-at document-end
// @license MIT
// @downloadURL none
// ==/UserScript==
// ==UserScript==
(function() {
'use strict';
// 声明使用繁化姬API的提示信息
console.log("本程序使用了繁化姬的API服务 - 繁化姬商用必须付费 - https://zhconvert.org");
// 获取用户设置,默认为自动模式关闭
let autoMode = GM_getValue('fanhuaji_auto_mode', false);
// 记录当前转换状态:null=未转换,'simplified'=已转为简体,'traditional'=已转为繁体
let convertStatus = GM_getValue('fanhuaji_status_' + window.location.hostname, null);
// 记录原始文本,用于双向转换
const originalTexts = new Map();
// 创建一个按钮,用于控制转换功能
const createControlButton = () => {
const controlButton = document.createElement('div');
controlButton.id = 'fanhuaji-toggle';
controlButton.style.position = 'fixed';
controlButton.style.bottom = '20px';
controlButton.style.right = '20px';
controlButton.style.padding = '10px';
controlButton.style.backgroundColor = '#f0f0f0';
controlButton.style.border = '1px solid #ccc';
controlButton.style.borderRadius = '5px';
controlButton.style.cursor = 'pointer';
controlButton.style.zIndex = '9999';
controlButton.style.fontSize = '14px';
controlButton.style.fontFamily = 'Arial, sans-serif';
controlButton.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)';
controlButton.title = "本程序使用了繁化姬的API服务 - 繁化姬商用必须付费";
updateButtonState(controlButton);
// 添加点击事件
controlButton.addEventListener('click', function(e) {
if (e.altKey) {
// Alt+点击切换自动/手动模式
autoMode = !autoMode;
GM_setValue('fanhuaji_auto_mode', autoMode);
updateButtonState(controlButton);
showNotification(autoMode ? "已切换到自动模式" : "已切换到手动模式");
} else {
// 普通点击执行转换
toggleConversion(controlButton);
}
// 点击视觉反馈
controlButton.style.backgroundColor = '#e0e0e0';
setTimeout(() => {
controlButton.style.backgroundColor = '#f0f0f0';
}, 300);
});
// 添加繁化姬官网链接
const linkElement = document.createElement('a');
linkElement.href = 'https://zhconvert.org';
linkElement.target = '_blank';
linkElement.style.position = 'fixed';
linkElement.style.bottom = '10px';
linkElement.style.right = '20px';
linkElement.style.fontSize = '10px';
linkElement.style.color = '#999';
linkElement.style.textDecoration = 'none';
linkElement.style.zIndex = '9999';
linkElement.textContent = '繁化姬官网';
document.body.appendChild(controlButton);
document.body.appendChild(linkElement);
// 添加自动模式切换提示
const tooltipElement = document.createElement('div');
tooltipElement.style.position = 'fixed';
tooltipElement.style.bottom = '50px';
tooltipElement.style.right = '20px';
tooltipElement.style.fontSize = '10px';
tooltipElement.style.color = '#999';
tooltipElement.style.zIndex = '9999';
tooltipElement.textContent = 'Alt+点击切换自动/手动模式';
document.body.appendChild(tooltipElement);
return controlButton;
};
// 更新按钮状态
const updateButtonState = (button) => {
const modeText = autoMode ? "自动" : "手动";
// 根据当前转换状态更新按钮文本
if (convertStatus === 'simplified') {
button.innerHTML = `简→繁 (${modeText})
by 繁化姬`;
} else if (convertStatus === 'traditional') {
button.innerHTML = `繁→简 (${modeText})
by 繁化姬`;
} else {
button.innerHTML = `繁→简 (${modeText})
by 繁化姬`;
}
};
// 切换转换状态
const toggleConversion = (button) => {
if (convertStatus === null || convertStatus === 'traditional') {
// 当前是原始状态或繁体状态,转为简体
convertPageTo('Simplified');
convertStatus = 'simplified';
} else {
// 当前是简体状态,转为繁体
convertPageTo('Traditional');
convertStatus = 'traditional';
}
// 保存当前状态
GM_setValue('fanhuaji_status_' + window.location.hostname, convertStatus);
updateButtonState(button);
};
// 显示通知
const showNotification = (message) => {
const notification = document.createElement('div');
notification.style.position = 'fixed';
notification.style.top = '20px';
notification.style.right = '20px';
notification.style.padding = '10px 15px';
notification.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
notification.style.color = 'white';
notification.style.borderRadius = '5px';
notification.style.zIndex = '10000';
notification.style.fontSize = '14px';
notification.style.fontFamily = 'Arial, sans-serif';
notification.style.opacity = '1';
notification.style.transition = 'opacity 0.5s';
notification.style.boxShadow = '0 2px 10px rgba(0,0,0,0.2)';
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.style.opacity = '0';
setTimeout(() => {
if (notification.parentNode) {
document.body.removeChild(notification);
}
}, 500);
}, 2000);
};
// 使用繁化姬API转换文本
const convertTextViaAPI = (text, converter, callback) => {
if (!text || text.trim() === '') {
callback('');
return;
}
GM_xmlhttpRequest({
method: 'POST',
url: 'https://api.zhconvert.org/convert',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data: 'text=' + encodeURIComponent(text) + '&converter=' + converter,
onload: function(response) {
try {
const result = JSON.parse(response.responseText);
if (result.code === 0) {
callback(result.data.text);
} else {
console.error('繁化姬API错误:', result.msg);
callback(text);
}
} catch (e) {
console.error('解析API响应出错:', e);
callback(text);
}
},
onerror: function(error) {
console.error('API请求失败:', error);
callback(text);
}
});
};
// 获取页面所有文本内容
const getPageText = () => {
// 获取body中的所有文本节点内容
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT,
{
acceptNode: function(node) {
// 排除script和style标签中的内容
if (node.parentNode.tagName === 'SCRIPT' ||
node.parentNode.tagName === 'STYLE' ||
node.parentNode.tagName === 'NOSCRIPT' ||
node.parentNode.id === 'fanhuaji-toggle') {
return NodeFilter.FILTER_REJECT;
}
// 如果节点内容不为空,接受该节点
if (node.nodeValue.trim() !== '') {
return NodeFilter.FILTER_ACCEPT;
}
return NodeFilter.FILTER_SKIP;
}
}
);
const textNodes = [];
let currentNode;
while (currentNode = walker.nextNode()) {
textNodes.push(currentNode);
}
return textNodes;
};
// 批量处理文本节点以减少API调用次数
const processTextNodesInBatches = (textNodes, converter, batchSize = 20) => {
if (textNodes.length === 0) return;
// 显示转换进度
showNotification(`正在转换中 (0/${textNodes.length})`);
let processedCount = 0;
let lastUpdateTime = Date.now();
// 将文本节点分组
for (let i = 0; i < textNodes.length; i += batchSize) {
const batch = textNodes.slice(i, i + batchSize);
// 合并批次中的文本内容,使用特殊分隔符
const separator = '|||||';
const combinedText = batch.map(node => node.nodeValue).join(separator);
// 调用API转换组合文本
convertTextViaAPI(combinedText, converter, (convertedText) => {
// 分割转换后的文本
const convertedParts = convertedText.split(separator);
// 更新各个节点的文本内容
for (let j = 0; j < batch.length && j < convertedParts.length; j++) {
const node = batch[j];
const nodeId = getNodeId(node);
// 如果是首次转换,保存原始文本
if (!originalTexts.has(nodeId)) {
originalTexts.set(nodeId, node.nodeValue);
}
// 更新节点文本
node.nodeValue = convertedParts[j];
}
// 更新进度
processedCount += batch.length;
// 每秒最多更新一次进度通知,避免频繁DOM操作
const currentTime = Date.now();
if (currentTime - lastUpdateTime > 1000) {
const percent = Math.round((processedCount / textNodes.length) * 100);
showNotification(`正在转换中 (${processedCount}/${textNodes.length}, ${percent}%)`);
lastUpdateTime = currentTime;
}
// 所有批次处理完成后显示成功消息
if (processedCount >= textNodes.length) {
showNotification("转换完成!");
}
});
}
};
// 为节点生成唯一ID
const getNodeId = (node) => {
if (!node._fanhuajiId) {
node._fanhuajiId = 'node_' + Math.random().toString(36).substr(2, 9);
}
return node._fanhuajiId;
};
// 转换页面内容到指定格式
const convertPageTo = (converter) => {
const textNodes = getPageText();
processTextNodesInBatches(textNodes, converter);
};
// 恢复原始文本
const restoreOriginalText = () => {
for (const [nodeId, originalText] of originalTexts.entries()) {
const node = document.querySelector(`[_fanhuajiId="${nodeId}"]`);
if (node) {
node.nodeValue = originalText;
}
}
originalTexts.clear();
};
// 检测页面是否主要是繁体中文
const isMainlyTraditionalChinese = () => {
const sampleText = getPageText().slice(0, 100).map(node => node.nodeValue).join('');
// 常见的繁体字符
const traditionalChars = '國會學實點體處義務發書樣說語認個開關壹貳參數當爲隻來經與這產紅專麼麗樂營廠鄉兒內馬軍區頭鳥長門問無車電話雞師歲';
// 对应的简体字符
const simplifiedChars = '国会学实点体处义务发书样说语认个开关一二三数当为只来经与这产红专么丽乐营厂乡儿内马军区头鸟长门问无车电话鸡师岁';
let traditionalCount = 0;
let simplifiedCount = 0;
// 计算繁体和简体字符的数量
for (const char of sampleText) {
const traditionalIndex = traditionalChars.indexOf(char);
if (traditionalIndex !== -1) {
traditionalCount++;
} else if (simplifiedChars.indexOf(char) !== -1) {
simplifiedCount++;
}
}
// 根据比例判断页面主要是繁体还是简体
return traditionalCount > simplifiedCount && traditionalCount > 5;
};
// 监听动态加载的内容
const observeDynamicContent = () => {
const observer = new MutationObserver((mutations) => {
if (!autoMode || !convertStatus) return;
let newTextNodes = [];
for (const mutation of mutations) {
if (mutation.type === 'childList') {
for (const node of mutation.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE) {
const textNodesInNewContent = getTextNodesIn(node);
newTextNodes = newTextNodes.concat(textNodesInNewContent);
}
}
}
}
if (newTextNodes.length > 0) {
const converter = convertStatus === 'simplified' ? 'Simplified' : 'Traditional';
processTextNodesInBatches(newTextNodes, converter);
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
};
// 获取元素中的所有文本节点
const getTextNodesIn = (element) => {
const walker = document.createTreeWalker(
element,
NodeFilter.SHOW_TEXT,
{
acceptNode: function(node) {
if (node.parentNode.tagName === 'SCRIPT' ||
node.parentNode.tagName === 'STYLE' ||
node.parentNode.tagName === 'NOSCRIPT' ||
node.parentNode.id === 'fanhuaji-toggle') {
return NodeFilter.FILTER_REJECT;
}
if (node.nodeValue.trim() !== '') {
return NodeFilter.FILTER_ACCEPT;
}
return NodeFilter.FILTER_SKIP;
}
}
);
const textNodes = [];
let currentNode;
while (currentNode = walker.nextNode()) {
textNodes.push(currentNode);
}
return textNodes;
};
// 初始化
const init = () => {
// 创建控制按钮
const controlButton = createControlButton();
// 如果启用了自动模式且页面主要是繁体中文,自动转换为简体
if (autoMode && convertStatus === null && isMainlyTraditionalChinese()) {
convertPageTo('Simplified');
convertStatus = 'simplified';
GM_setValue('fanhuaji_status_' + window.location.hostname, convertStatus);
updateButtonState(controlButton);
}
// 监听动态内容
observeDynamicContent();
};
// 当页面加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();