Utility for optimizing web data display.
// ==UserScript==
// @name pk Web Data Optimizer
// @namespace http://tampermonkey.net/
// @version 1.8
// @description Utility for optimizing web data display.
// @author bbk
// @match https://parks2.bandainamco-am.co.jp/member_mypage.html*
// @match https://parks2.bandainamco-am.co.jp/admission_use_ticket.html*
// @match https://parks2.bandainamco-am.co.jp/member_history.html*
// @match https://parks2.bandainamco-am.co.jp/member_regist.html*
// @license MIT
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
// ==================== 配置区域 ====================
// 默认替换规则:标签 -> 替换值
const DEFAULT_REPLACEMENTS = {
'氏名(漢字)': '山田 太郎',
'氏名(カナ)': 'ヤマダ タロウ',
'生年月日': '1990/01/01',
'性別': '男性',
'郵便番号': '100-0001',
'都道府県': '東京都',
'市区町村': '千代田区千代田',
'丁目・番地': '1-1-1',
'電話番号': '09012345678'
};
// 需要替换的标签列表(按顺序排列)
const TARGET_LABELS = [
'氏名(漢字)',
'氏名(カナ)',
'生年月日',
'性別',
'郵便番号',
'都道府県',
'市区町村',
'丁目・番地',
'電話番号'
];
// 性别选项
const GENDER_OPTIONS = [
'男性',
'女性',
'あてはまらない',
'回答しない/非表示'
];
// localStorage 键名
const STORAGE_KEY = 'personal_info_replacements';
// ==================== 初始防闪烁处理 ====================
// 核心思想:在元素被替换前保持不可见,替换后通过 data-replaced 属性显示
(function injectHidingStyle() {
const style = document.createElement('style');
style.id = 'hide-member-info-initial';
style.textContent = `
/* 初始隐藏目标元素 */
.block-mypage-member-info-value:not([data-replaced="true"]),
.block-mypage-coupon-list-item-code-value:not([data-replaced="true"]),
.block-mypage-history-block-detail-contents-block-content:not([data-replaced="true"]),
.form-table td .form-input:not([data-replaced="true"]),
.form-table td .form-input-label:not([data-replaced="true"]),
.form-table td .text:not([data-replaced="true"]) {
opacity: 0 !important;
}
/* 替换后显示,带一点淡入效果 */
.block-mypage-member-info-value[data-replaced="true"],
.block-mypage-coupon-list-item-code-value[data-replaced="true"],
.block-mypage-history-block-detail-contents-block-content[data-replaced="true"],
.form-table td .form-input[data-replaced="true"],
.form-table td .form-input-label[data-replaced="true"],
.form-table td .text[data-replaced="true"] {
opacity: 1 !important;
transition: opacity 0.2s ease-in-out;
}
`;
if (document.documentElement) {
document.documentElement.appendChild(style);
} else {
const observer = new MutationObserver(() => {
if (document.documentElement) {
document.documentElement.appendChild(style);
observer.disconnect();
}
});
observer.observe(document, { childList: true, subtree: true });
}
})();
// ==================== 数据管理 ====================
/**
* 从 localStorage 加载替换规则
*/
function loadReplacements() {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
return JSON.parse(stored);
}
} catch (e) {
console.error('[信息替换] 读取 localStorage 失败:', e);
}
return { ...DEFAULT_REPLACEMENTS };
}
/**
* 保存替换规则到 localStorage
*/
function saveReplacements(replacements) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(replacements));
console.log('[信息替换] 已保存到 localStorage');
return true;
} catch (e) {
console.error('[信息替换] 保存到 localStorage 失败:', e);
return false;
}
}
/**
* 重置为页面当前显示的数据
*/
function resetReplacements() {
const pageData = extractCurrentDataFromPage();
// 如果页面没有数据(比如不在个人信息页),则使用默认配置
const newData = Object.keys(pageData).length > 0 ? pageData : { ...DEFAULT_REPLACEMENTS };
saveReplacements(newData);
console.log('[信息替换] 已根据页面数据重置初始值');
return newData;
}
/**
* 从页面提取当前显示的数据
*/
function extractCurrentDataFromPage() {
const extracted = {};
// --- 1. 从个人信息页提取 ---
const dts = document.querySelectorAll('dt.block-mypage-member-info-label');
dts.forEach(dt => {
const labelText = dt.textContent.trim();
if (TARGET_LABELS.includes(labelText)) {
const dd = dt.nextElementSibling;
if (dd && dd.classList.contains('block-mypage-member-info-value')) {
// 优先从已保存的原始值属性中提取,否则提取当前文本
if (dd.hasAttribute('data-original-value')) {
extracted[labelText] = dd.getAttribute('data-original-value');
} else if (labelText === '性別') {
const span = dd.querySelector('span');
extracted[labelText] = span ? span.textContent.trim() : dd.textContent.trim();
} else {
extracted[labelText] = dd.textContent.trim();
}
}
}
});
// --- 2. 从入场券详情页提取姓名 (如果个人信息页没提取到) ---
if (!extracted['氏名(漢字)']) {
const ticketNameElem = document.querySelector('.block-mypage-coupon-list-item-code-value');
if (ticketNameElem) {
extracted['氏名(漢字)'] = ticketNameElem.hasAttribute('data-original-value')
? ticketNameElem.getAttribute('data-original-value')
: ticketNameElem.textContent.trim();
}
}
// --- 3. 从订单历史详情页提取姓名/地址/电话 ---
const historyBlocks = document.querySelectorAll('dl.block-mypage-history-block-detail-contents-block');
historyBlocks.forEach(block => {
const dt = block.querySelector('.block-mypage-history-block-detail-contents-block-label');
const dd = block.querySelector('.block-mypage-history-block-detail-contents-block-content');
if (!dt || !dd) return;
const labelText = dt.textContent.trim();
if (labelText === 'ご依頼主' && !extracted['氏名(漢字)']) {
extracted['氏名(漢字)'] = dd.hasAttribute('data-original-value')
? dd.getAttribute('data-original-value')
: dd.textContent.trim();
}
if (labelText === 'お届け先') {
const rawValue = dd.hasAttribute('data-original-value')
? dd.getAttribute('data-original-value')
: getMultilineText(dd);
const parsed = parseHistoryRecipientText(rawValue);
Object.keys(parsed).forEach(key => {
if (!extracted[key] && parsed[key]) {
extracted[key] = parsed[key];
}
});
}
});
// --- 4. 从会员信息编辑页提取 ---
const formTable = document.querySelector('.form-table');
if (formTable) {
const rows = formTable.querySelectorAll('tr');
rows.forEach(row => {
const labelElem = row.querySelector('.form-title-label');
if (!labelElem) return;
const labelText = labelElem.textContent.trim();
if (labelText === '氏名(漢字)') {
const lName = document.getElementById('L_NAME')?.value || '';
const fName = document.getElementById('F_NAME')?.value || '';
extracted[labelText] = `${lName} ${fName}`.trim();
} else if (labelText === '氏名(カナ)') {
const lKana = document.getElementById('L_KANA')?.value || '';
const fKana = document.getElementById('F_KANA')?.value || '';
extracted[labelText] = `${lKana} ${fKana}`.trim();
} else if (labelText === '郵便番号') {
extracted[labelText] = document.getElementById('ZIP')?.value || '';
} else if (labelText === '都道府県') {
extracted[labelText] = document.getElementById('ADDR1')?.value || '';
} else if (labelText === '市区町村') {
extracted[labelText] = document.getElementById('ADDR2')?.value || '';
} else if (labelText === '丁目・番地') {
extracted[labelText] = document.getElementById('MEMBER.FREE_ITEM16')?.value || '';
} else if (labelText === '携帯電話番号(SMS認証)') {
extracted['電話番号'] = document.getElementById('TEL')?.value || '';
} else if (labelText === '生年月日') {
const birthLabel = row.querySelector('.form-input-label');
if (birthLabel) {
extracted[labelText] = birthLabel.textContent.trim();
}
} else if (labelText === '性別') {
const genderDiv = row.querySelector('.form-input');
if (genderDiv) {
extracted[labelText] = genderDiv.textContent.trim();
}
}
});
}
return extracted;
}
// ==================== 核心替换逻辑 ====================
/**
* 针对你提供的 HTML 结构,精确替换会员信息
*/
function replaceMemberInfo(replacements) {
// --- 1. 处理个人信息页 (dt/dd 结构) ---
const dts = document.querySelectorAll('dt.block-mypage-member-info-label');
dts.forEach(dt => {
const labelText = dt.textContent.trim();
// 为“氏名(漢字)”添加双击打开设置面板的功能
if (labelText === '氏名(漢字)') {
setupDblClick(dt);
}
// 如果是我们需要替换的标签
if (TARGET_LABELS.includes(labelText)) {
const dd = dt.nextElementSibling;
if (dd && dd.classList.contains('block-mypage-member-info-value')) {
// 如果已经处理过,直接跳过,防止 MutationObserver 无限循环
if (dd.hasAttribute('data-replaced')) return;
// 核心:在任何替换发生前,如果尚未保存原始值,则保存它
saveOriginalValue(dd, labelText);
const replacement = replacements[labelText];
// 仅当替换值不为空时执行替换
if (replacement !== undefined && replacement.trim() !== '') {
applyValue(dd, labelText, replacement);
} else {
// 即使不替换,也要标记为已处理,以便 CSS 显示它
dd.setAttribute('data-replaced', 'true');
}
}
} else {
// 对于不需要修改的标签(如邮箱、ID等),也需要标记为已处理,否则会被 CSS 隐藏
const dd = dt.nextElementSibling;
if (dd && dd.classList.contains('block-mypage-member-info-value')) {
if (!dd.hasAttribute('data-replaced')) {
dd.setAttribute('data-replaced', 'true');
}
}
}
});
// --- 2. 处理入场券详情页 (特定 class) ---
const ticketNameElem = document.querySelector('.block-mypage-coupon-list-item-code-value');
if (ticketNameElem && !ticketNameElem.hasAttribute('data-replaced')) {
setupDblClick(ticketNameElem);
saveOriginalValue(ticketNameElem, '氏名(漢字)');
const replacement = replacements['氏名(漢字)'];
if (replacement !== undefined && replacement.trim() !== '') {
ticketNameElem.textContent = replacement;
}
// 无论是否替换,都标记为已处理以显示内容
ticketNameElem.setAttribute('data-replaced', 'true');
}
// --- 3. 处理订单历史详情页 ---
const historyBlocks = document.querySelectorAll('dl.block-mypage-history-block-detail-contents-block');
historyBlocks.forEach(block => {
const dt = block.querySelector('.block-mypage-history-block-detail-contents-block-label');
const dd = block.querySelector('.block-mypage-history-block-detail-contents-block-content');
if (!dt || !dd) return;
const labelText = dt.textContent.trim();
if (labelText === 'ご依頼主' || labelText === 'お届け先') {
setupDblClick(dt);
}
if (dd.hasAttribute('data-replaced')) return;
if (labelText === 'ご依頼主') {
saveOriginalValue(dd, '氏名(漢字)');
const replacement = replacements['氏名(漢字)'];
if (replacement !== undefined && replacement.trim() !== '') {
dd.textContent = replacement;
}
dd.setAttribute('data-replaced', 'true');
return;
}
if (labelText === 'お届け先') {
saveOriginalValue(dd, 'お届け先');
applyHistoryRecipientValue(dd, replacements);
dd.setAttribute('data-replaced', 'true');
return;
}
dd.setAttribute('data-replaced', 'true');
});
// --- 4. 处理会员信息编辑页 (form-table 结构) ---
const formTable = document.querySelector('.form-table');
if (formTable) {
const rows = formTable.querySelectorAll('tr');
rows.forEach(row => {
const labelElem = row.querySelector('.form-title-label');
if (!labelElem) return;
const labelText = labelElem.textContent.trim();
// 为“氏名(漢字)”等标签添加双击打开设置面板的功能
if (labelText === '氏名(漢字)' || labelText === '氏名(カナ)') {
setupDblClick(labelElem);
}
if (labelText === '氏名(漢字)') {
const val = replacements[labelText];
const [lName, fName] = splitName(val);
updateEditField('L_NAME', lName, row);
updateEditField('F_NAME', fName, row);
} else if (labelText === '氏名(カナ)') {
const val = replacements[labelText];
const [lKana, fKana] = splitName(val);
updateEditField('L_KANA', lKana, row);
updateEditField('F_KANA', fKana, row);
} else if (labelText === '郵便番号') {
updateEditField('ZIP', replacements[labelText], row);
} else if (labelText === '都道府県') {
updateEditField('ADDR1', replacements[labelText], row);
} else if (labelText === '市区町村') {
updateEditField('ADDR2', replacements[labelText], row);
} else if (labelText === '丁目・番地') {
updateEditField('MEMBER.FREE_ITEM16', replacements[labelText], row);
} else if (labelText === '携帯電話番号(SMS認証)') {
updateEditField('TEL', replacements['電話番号'], row);
} else if (labelText === '生年月日') {
const birthLabel = row.querySelector('.form-input-label');
if (birthLabel && !birthLabel.hasAttribute('data-replaced')) {
birthLabel.textContent = replacements[labelText];
birthLabel.setAttribute('data-replaced', 'true');
}
} else if (labelText === '性別') {
const genderDiv = row.querySelector('.form-input');
if (genderDiv && !genderDiv.hasAttribute('data-replaced')) {
genderDiv.textContent = replacements[labelText];
genderDiv.setAttribute('data-replaced', 'true');
}
}
// 标记该行的所有 .form-input 为已处理,以便 CSS 显示
row.querySelectorAll('.form-input, .form-input-label, .text').forEach(elem => {
if (!elem.hasAttribute('data-replaced')) {
elem.setAttribute('data-replaced', 'true');
}
});
});
}
}
/**
* 更新编辑页面的字段(input/select/span)
*/
function updateEditField(id, value, row) {
const field = document.getElementById(id);
if (!field) return;
// 如果已经处理过,直接跳过
if (field.hasAttribute('data-replaced')) return;
// 保存原始值
if (!field.hasAttribute('data-original-value')) {
field.setAttribute('data-original-value', field.value || '');
}
if (value !== undefined && value !== null) {
field.value = value;
}
field.setAttribute('data-replaced', 'true');
// 同时更新同容器内的 span.text (如果有)
const container = field.closest('.form-input');
if (container) {
const spanText = container.querySelector('.text');
if (spanText) {
if (!spanText.hasAttribute('data-original-value')) {
spanText.setAttribute('data-original-value', spanText.textContent.trim());
}
spanText.textContent = value || '';
spanText.setAttribute('data-replaced', 'true');
}
container.setAttribute('data-replaced', 'true');
}
}
/**
* 辅助:拆分姓和名
*/
function splitName(fullName) {
if (!fullName) return ['', ''];
const parts = fullName.trim().split(/\s+/);
if (parts.length >= 2) {
return [parts[0], parts.slice(1).join(' ')];
}
return [fullName, ''];
}
/**
* 保存原始值到 data 属性
*/
function saveOriginalValue(elem, labelText) {
if (!elem.hasAttribute('data-original-value')) {
const originalVal = getOriginalValue(elem, labelText);
elem.setAttribute('data-original-value', originalVal);
}
}
function getOriginalValue(elem, labelText) {
if (labelText === '性別' && elem.querySelector('span')) {
return elem.querySelector('span').textContent.trim();
}
if (labelText === 'お届け先') {
return getMultilineText(elem);
}
return elem.textContent.trim();
}
/**
* 应用替换值
*/
function applyValue(elem, labelText, replacement) {
if (labelText === '性別') {
const span = elem.querySelector('span');
if (span) {
span.textContent = replacement;
} else {
elem.textContent = replacement;
}
} else {
elem.textContent = replacement;
}
// 标记已替换,CSS 将使其可见
elem.setAttribute('data-replaced', 'true');
}
function getMultilineText(elem) {
return elem.innerHTML
.replace(/<br\s*\/?>/gi, '\n')
.replace(/ /gi, ' ')
.replace(/<[^>]+>/g, '')
.split('\n')
.map(line => line.trim())
.filter(line => line !== '')
.join('\n')
.trim();
}
function parseHistoryRecipientText(text) {
const extracted = {};
const lines = String(text || '')
.split('\n')
.map(line => line.trim())
.filter(line => line !== '');
if (lines.length === 0) {
return extracted;
}
const nameLine = lines[0] || '';
const nameMatch = nameLine.match(/^(.*?)\s*\((.*?)\)$/);
if (nameMatch) {
extracted['氏名(漢字)'] = nameMatch[1].trim();
extracted['氏名(カナ)'] = nameMatch[2].trim();
} else if (nameLine) {
extracted['氏名(漢字)'] = nameLine;
}
if (lines[1]) {
extracted['郵便番号'] = lines[1];
}
if (lines[2]) {
const addressParts = splitAddress(lines[2]);
Object.assign(extracted, addressParts);
}
if (lines[3]) {
extracted['電話番号'] = lines[3];
}
return extracted;
}
function splitAddress(addressText) {
const extracted = {};
const address = String(addressText || '').trim();
if (!address) {
return extracted;
}
const prefectureMatch = address.match(/^(.+?[都道府県])\s*(.*)$/);
if (prefectureMatch) {
extracted['都道府県'] = prefectureMatch[1].trim();
const remaining = prefectureMatch[2].trim();
const numberIndex = remaining.search(/\d/);
if (numberIndex >= 0) {
extracted['市区町村'] = remaining.slice(0, numberIndex).trim();
extracted['丁目・番地'] = remaining.slice(numberIndex).trim();
} else {
extracted['市区町村'] = remaining;
}
return extracted;
}
extracted['市区町村'] = address;
return extracted;
}
function formatHistoryRecipient(replacements) {
const nameKanji = (replacements['氏名(漢字)'] || '').trim();
const nameKana = (replacements['氏名(カナ)'] || '').trim();
const postalCode = (replacements['郵便番号'] || '').trim();
const prefecture = (replacements['都道府県'] || '').trim();
const city = (replacements['市区町村'] || '').trim();
const street = (replacements['丁目・番地'] || '').trim();
const phone = (replacements['電話番号'] || '').trim();
const address = `${prefecture}${prefecture && (city || street) ? ' ' : ''}${city}${street}`.trim();
const nameLine = nameKanji && nameKana ? `${nameKanji}(${nameKana})` : (nameKanji || nameKana);
return [nameLine, postalCode, address, phone]
.filter(line => line !== '')
.map(line => escapeHtml(line))
.join('<br>');
}
function applyHistoryRecipientValue(elem, replacements) {
const formatted = formatHistoryRecipient(replacements);
if (formatted) {
elem.innerHTML = formatted;
}
}
/**
* 设置双击打开面板
*/
function setupDblClick(elem) {
if (!elem.hasAttribute('data-has-dblclick')) {
elem.style.cursor = 'pointer';
elem.title = '双击打开替换设置';
elem.addEventListener('dblclick', (e) => {
e.preventDefault();
createSettingsPanel();
});
elem.setAttribute('data-has-dblclick', 'true');
}
}
// ==================== 动态内容监听 ====================
/**
* 使用 MutationObserver 监听 DOM 变化,处理动态加载的内容
*/
function setupMutationObserver() {
// 使用 document.documentElement 可以在 body 出来前就开始观察
const target = document.documentElement || document;
let rafId = null;
const observer = new MutationObserver((mutations) => {
// 检查是否有子节点变化,避免不必要的触发
const hasAddedNodes = mutations.some(m => m.addedNodes.length > 0);
if (!hasAddedNodes) return;
// 使用 requestAnimationFrame 进行限流,并防止同步死循环
if (rafId) cancelAnimationFrame(rafId);
rafId = requestAnimationFrame(() => {
applyReplacements();
rafId = null;
});
});
observer.observe(target, {
childList: true,
subtree: true
});
return observer;
}
// ==================== 用户界面 ====================
/**
* 创建设置面板
*/
function createSettingsPanel() {
// 移除已存在的面板
const existing = document.getElementById('personal-info-replacer-panel');
if (existing) existing.remove();
const replacements = loadReplacements();
const panel = document.createElement('div');
panel.id = 'personal-info-replacer-panel';
panel.innerHTML = `
<div style="
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
border: 2px solid #333;
border-radius: 8px;
padding: 20px;
z-index: 999999;
width: 400px;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
font-family: sans-serif;
">
<h3 style="margin-top:0;border-bottom:1px solid #ccc;padding-bottom:10px;text-align:center;">
🔒 个人信息替换设置
</h3>
<div style="margin-bottom:15px;">
<p style="font-size:12px;color:#666;margin-bottom:10px;">请设置各项个人信息的替换内容:</p>
<div id="replacer-fields-container">
${TARGET_LABELS.map(label => {
if (label === '性別') {
return `
<div style="margin-bottom:10px; display: flex; align-items: center;">
<label style="width: 120px; font-size: 13px; font-weight: bold;">${label}:</label>
<select class="field-input" data-label="${label}" style="flex: 1; padding: 5px; border: 1px solid #ccc; border-radius: 4px;">
${GENDER_OPTIONS.map(opt => `<option value="${opt}" ${replacements[label] === opt ? 'selected' : ''}>${opt}</option>`).join('')}
</select>
</div>
`;
} else {
return `
<div style="margin-bottom:10px; display: flex; align-items: center;">
<label style="width: 120px; font-size: 13px; font-weight: bold;">${label}:</label>
<input type="text" class="field-input" data-label="${label}" value="${escapeHtml(replacements[label] || '')}"
placeholder="可放空"
style="flex: 1; padding: 5px; border: 1px solid #ccc; border-radius: 4px;">
</div>
`;
}
}).join('')}
</div>
</div>
<div style="text-align:center; margin-top: 20px; padding-top: 15px; border-top: 1px solid #eee;">
<button id="save-rules-btn" style="padding:8px 25px;margin-right:10px;cursor:pointer;background:#4caf50;color:white;border:none;border-radius:4px;font-weight:bold;">保存并应用</button>
<button id="reset-rules-btn" style="padding:8px 15px;margin-right:10px;cursor:pointer;background:#2196f3;color:white;border:none;border-radius:4px;">重置当前页面数据</button>
<button id="close-panel-btn" style="padding:8px 15px;cursor:pointer;background:#9e9e9e;color:white;border:none;border-radius:4px;">取消</button>
</div>
</div>
<div style="
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 999998;
" id="replacer-overlay"></div>
`;
document.body.appendChild(panel);
// 绑定事件
document.getElementById('close-panel-btn').addEventListener('click', () => panel.remove());
document.getElementById('replacer-overlay').addEventListener('click', () => panel.remove());
document.getElementById('save-rules-btn').addEventListener('click', () => {
const newReplacements = {};
document.querySelectorAll('.field-input').forEach(input => {
const label = input.dataset.label;
newReplacements[label] = input.value.trim();
});
saveReplacements(newReplacements);
reapplyReplacements();
panel.remove();
alert('设置已保存并应用');
});
document.getElementById('reset-rules-btn').addEventListener('click', () => {
if (confirm('确定要从当前页面提取数据作为初始值吗?\n(这会覆盖当前的设置)')) {
resetReplacements();
panel.remove();
createSettingsPanel();
// 提取后不需要立即 apply,因为提取的就是当前页面的值
}
});
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// ==================== 主流程 ====================
/**
* 应用替换
*/
function applyReplacements() {
const replacements = loadReplacements();
replaceMemberInfo(replacements);
}
function reapplyReplacements() {
document.querySelectorAll(
'.block-mypage-member-info-value[data-replaced], ' +
'.block-mypage-coupon-list-item-code-value[data-replaced], ' +
'.block-mypage-history-block-detail-contents-block-content[data-replaced], ' +
'.form-table [data-replaced]'
).forEach(elem => {
elem.removeAttribute('data-replaced');
});
applyReplacements();
}
/**
* 初始化
*/
function init() {
// 首次运行时初始化 localStorage
if (!localStorage.getItem(STORAGE_KEY)) {
saveReplacements({ ...DEFAULT_REPLACEMENTS });
}
// 立即尝试替换一次
applyReplacements();
// 注册油猴菜单命令
if (typeof GM_registerMenuCommand !== 'undefined') {
GM_registerMenuCommand('🔧 打开替换设置', createSettingsPanel);
GM_registerMenuCommand('🔄 立即重新替换', reapplyReplacements);
}
console.log('[信息替换] 脚本初始化完成,当前规则:', loadReplacements());
}
// 启动早期观察器 (document-start 级别)
setupMutationObserver();
// 页面加载阶段性初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();