// ==UserScript== // @name X (Twitter) 用户名 / 账号名 自定义 // @namespace local.pi-desktop.x-rename // @version 1.0.0 // @description 把 X 上指定账号的显示名和 @账号名替换成你在代码里写死的自定义内容(只改显示,不改链接与请求) // @author yunbiu // @match https://x.com/* // @match https://www.x.com/* // @match https://twitter.com/* // @match https://www.twitter.com/* // @match https://mobile.x.com/* // @match https://mobile.twitter.com/* // @grant none // @run-at document-idle // @noframes // @license MIT // ==/UserScript== /* 用法 ---- 1. 脚本装好后,在 Tampermonkey 里点「编辑」,找到下面的 CONFIG。 2. 把 rules 里每条规则的 to 改成你想看到的内容即可;from 是 X 上真实的内容。 要同时改多个账号,就在 rules 里多加几行(同一个账号加一条 name + 一条 handle)。 3. Ctrl+S 保存,再刷新 X 页面(或直接切换页面会自动生效)。 说明 ---- * name = 显示名(昵称),按「包含」匹配,所以名字里的这串字出现在任何位置都会被替换。 不要放过于常见的短词(比如 "AI"、"News"),会误伤一大片。 * handle = @账号名,按「完整单词」匹配,大小写不敏感;@ 前缀自动兼容(@my_new_handle → @你的新账号名)。 * 只改你眼睛看到的内容:链接 href、页面请求、发帖接口里的内容都不动, 点击头像/名字依然会跳到真实账号;输入框(发帖框、搜索框)里的文字也不会被改。 * 想还原:把 enabled 改成 false 或删掉脚本。 * to 里包含 from 也没问题(例如 my_new_handle → my_new_handle_v2),脚本只替换一次不会无限叠加。 * 排查问题:把 CONFIG.debug 改成 true,F12 控制台能看到每次替换;控制台执行 __xRename.rescan() 可强制重扫全页。 */ (function () { 'use strict'; /* ========================= 配置区(只改这里) ========================= */ const CONFIG = { // 总开关,false = 完全不生效 enabled: true, /** * 替换规则 * from : X 上原本显示的内容 * to : 你想让它显示成的内容 * type : 'name' → 显示名 / 昵称(包含匹配) * 'handle' → @账号名 / username(完整单词匹配,大小写不敏感) * * 下面两条就是根据你贴的侧边栏账号按钮来的: * 显示名 对方的显示名 → 自定义昵称 * 账号名 对方的账号名 → my_new_handle */ rules: [ { from: '对方的显示名', to: '自定义昵称', type: 'name' }, { from: '对方的账号名', to: 'my_new_handle', type: 'handle' }, // ↓ 多账号示例,按需要打开并改成真实的 from/to // { from: '对方的显示名', to: '想显示的名字', type: 'name' }, // { from: '对方的账号名', to: '想显示的账号', type: 'handle' }, ], // 是否一并替换 aria-label / title / alt 等属性(头像悬停提示、无障碍标签也用得上) rewriteAttributes: true, // 是否替换浏览器标签页标题,例如「对方的显示名 (@my_new_handle) / X」 rewriteTitle: true, // 排查问题时改成 true,会在控制台打印每次替换 debug: false, }; /* ======================= 配置区结束,下面不用改 ======================= */ if (!CONFIG.enabled) return; const WORD_CHAR = /[0-9A-Za-z_]/; const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const log = (...args) => { if (CONFIG.debug) console.log('%c[X改名]', 'color:#1d9bf0', ...args); }; /* ---------- 编译规则 ---------- */ const compiled = []; const seenFrom = new Set(); for (const raw of CONFIG.rules || []) { if (!raw || !raw.from || typeof raw.to !== 'string') continue; const from = String(raw.from); const to = raw.to; if (!from || from === to) continue; if (seenFrom.has(from.toLowerCase())) continue; seenFrom.add(from.toLowerCase()); compiled.push({ from, to, isHandle: raw.type === 'handle', re: new RegExp(escapeRe(from), 'gi'), }); } // 有效规则(同名规则已在上面按大小写去重) const active = compiled; // 防反复替换:记下「我们最后写进去的值」,下次再扫到同一个节点/属性时若内容没被别人改回来就跳过。 // 这样即使 to 里还包含 from(例如 my_new_handle → my_new_handle_v2)也不会越替换越长。 const appliedText = new WeakMap(); const appliedAttrs = new WeakMap(); let appliedTitle = null; /* ---------- 核心替换 ---------- */ function rewrite(value) { if (!value || typeof value !== 'string') return value; let out = value; for (const rule of active) { out = out.replace(rule.re, (match, offset, whole) => { if (rule.isHandle) { const before = whole[offset - 1]; const after = whole[offset + match.length]; if (before && WORD_CHAR.test(before)) return match; if (after && WORD_CHAR.test(after)) return match; } return rule.to; }); } return out; } /* ---------- DOM 处理 ---------- */ // 这些区域一律跳过:脚本/样式、输入控件、可编辑区域(发帖框、搜索框) const SKIP_SELECTOR = [ 'script', 'style', 'noscript', 'template', 'textarea', 'input', 'select', 'option', '[contenteditable="true"]', '[contenteditable=""]', '[role="textbox"]', '[role="searchbox"]', '[data-x-rename-skip]', ].join(','); const ATTRS = ['aria-label', 'title', 'alt', 'placeholder', 'data-screen-name', 'value']; function rewriteTextNode(node) { const raw = node.nodeValue; if (!raw || !raw.trim()) return; if (appliedText.get(node) === raw) return; // 还是我们上次写进去的值,什么都不用做 const next = rewrite(raw); if (next === raw) return; node.nodeValue = next; appliedText.set(node, next); log(JSON.stringify(raw), '→', JSON.stringify(next)); } function rewriteAttributes(el) { if (!el.getAttribute) return; const done = appliedAttrs.get(el) || {}; for (const name of ATTRS) { const raw = el.getAttribute(name); if (!raw) continue; if (done[name] === raw) continue; const next = rewrite(raw); if (next === raw) continue; el.setAttribute(name, next); done[name] = next; log(`[${name}]`, JSON.stringify(raw), '→', JSON.stringify(next)); } appliedAttrs.set(el, done); } function processTree(root) { if (!root) return; if (root.nodeType === Node.TEXT_NODE) { rewriteTextNode(root); return; } if (root.nodeType !== Node.ELEMENT_NODE) return; if (root.matches(SKIP_SELECTOR)) return; if (CONFIG.rewriteAttributes) rewriteAttributes(root); for (let child = root.firstChild; child; child = child.nextSibling) { if (child.nodeType === Node.TEXT_NODE) rewriteTextNode(child); else if (child.nodeType === Node.ELEMENT_NODE) processTree(child); } } function rewriteTitle() { if (!CONFIG.rewriteTitle) return; const raw = document.title; if (!raw) return; if (raw === appliedTitle) return; const next = rewrite(raw); if (next === raw) return; document.title = next; appliedTitle = next; log('[title]', JSON.stringify(raw), '→', JSON.stringify(next)); } /* ---------- 增量监听(X 是 React SPA,内容随时会被重渲染) ---------- */ const pending = new Set(); let timer = 0; function flush() { timer = 0; const batch = Array.from(pending); pending.clear(); for (const node of batch) { if (!node || !node.isConnected) continue; try { processTree(node); } catch (err) { if (CONFIG.debug) console.warn('[X改名] 处理节点失败', err); } } rewriteTitle(); } function schedule(node) { pending.add(node); if (timer) return; timer = setTimeout(flush, 16); } const observer = new MutationObserver((records) => { for (const record of records) { if (record.type === 'characterData') { schedule(record.target); continue; } if (record.type === 'childList') { for (const node of record.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE || node.nodeType === Node.TEXT_NODE) { schedule(node); } } } } }); function start() { if (!document.body) return; processTree(document.body); rewriteTitle(); observer.observe(document.documentElement, { childList: true, subtree: true, characterData: true, }); log('已启用,规则:', active.map((r) => `${r.from} → ${r.to}`)); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', start, { once: true }); } else { start(); } // 手动兜底:控制台执行 __xRename.rescan() 可强制重新扫描整页 try { window.__xRename = { rescan: () => { if (document.body) processTree(document.body); rewriteTitle(); }, rewrite, rules: active.map((r) => ({ from: r.from, to: r.to, type: r.isHandle ? 'handle' : 'name' })), }; } catch (_) { /* 页面可能禁用了 window 写入,忽略 */ } })();