Greasy Fork

来自缓存

Greasy Fork is available in English.

强密码生成器

在注册/表单页面生成高强度密码并提供复制/注入功能,支持偏好持久化与可选注入确认。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         强密码生成器
// @namespace    https://example.com/
// @version      0.1.0
// @description  在注册/表单页面生成高强度密码并提供复制/注入功能,支持偏好持久化与可选注入确认。
// @author       zskfree
// @match        *://*/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_setClipboard
// @grant        GM_registerMenuCommand
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    // 配置与默认值
    const DEFAULTS = {
        length: 16,
        useUpper: true,
        useLower: true,
        useNumbers: true,
        useSymbols: true,
        excludeSimilar: true,
        autoInject: false,
        showOnAllSites: false,
        showOnPasswordField: true,
        siteWhitelist: '',
        pos: null
    };

    const PREFS_KEY = '强密码生成器.prefs';
    const REGISTER_REGEX = /signup|register|create[-_ ]?account|createaccount|sign[-_ ]?up/i;
    const HOST_ID = 'gm-strong-password-root';
    const DRAG_THRESHOLD = 6;
    const TOAST_DURATION = 1800;
    const OBSERVE_TIMEOUT = 10000;

    let prefs = loadPrefs();
    let lastGenerated = '';

    // ========== 配置管理 ==========
    function loadPrefs() {
        try {
            const stored = GM_getValue(PREFS_KEY);
            if (stored) {
                return Object.assign({}, DEFAULTS, JSON.parse(stored));
            }
        } catch (error) {
            console.warn('加载偏好失败', error);
        }
        return Object.assign({}, DEFAULTS);
    }

    function savePrefs(newPrefs) {
        try {
            GM_setValue(PREFS_KEY, JSON.stringify(newPrefs));
        } catch (error) {
            console.warn('保存偏好失败', error);
        }
    }

    // ========== 站点匹配逻辑 ==========
    function hostMatchesWhitelist() {
        if (!prefs.siteWhitelist) {
            return false;
        }
        const hosts = prefs.siteWhitelist
            .split(',')
            .map(host => host.trim())
            .filter(Boolean);
        return hosts.some(host => {
            try {
                return location.hostname === host || location.hostname.endsWith('.' + host);
            } catch (error) {
                return false;
            }
        });
    }

    function shouldShowUI() {
        try {
            if (prefs.showOnAllSites) {
                return true;
            }
            if (hostMatchesWhitelist()) {
                return true;
            }
            if (prefs.showOnPasswordField && document.querySelector('input[type="password"]')) {
                return true;
            }
            if (REGISTER_REGEX.test(location.pathname + location.href)) {
                return true;
            }
        } catch (error) {
            // 忽略 DOM 查询错误
        }
        return false;
    }

    function observeForPasswordFields(timeout = OBSERVE_TIMEOUT) {
        if (!document.body) {
            return;
        }
        const startTime = Date.now();
        const observer = new MutationObserver(function checkPasswordFields(mutations, obs) {
            if (document.querySelector('input[type="password"]')) {
                ensureUI();
                obs.disconnect();
                return;
            }
            if (Date.now() - startTime > timeout) {
                obs.disconnect();
            }
        });
        observer.observe(document.body, { childList: true, subtree: true });
    }

    // ========== 菜单命令 ==========
    function addMenuCommands() {
        if (typeof GM_registerMenuCommand !== 'function') {
            return;
        }

        function registerCommand(label, callback) {
            try {
                GM_registerMenuCommand(label, callback);
            } catch (error) {
                console.warn('菜单命令注册失败:', error);
            }
        }

        registerCommand(
            `切换:在所有站点显示(当前:${prefs.showOnAllSites ? '是' : '否'})`,
            function toggleShowAllSites() {
                prefs.showOnAllSites = !prefs.showOnAllSites;
                savePrefs(prefs);
                const status = prefs.showOnAllSites ? '是' : '否';
                alert('设置已更新:在所有站点显示 = ' + status);
            }
        );

        registerCommand(
            '编辑站点白名单',
            function editWhitelist() {
                const current = prefs.siteWhitelist || '';
                const updated = prompt('用逗号分隔主机名(允许后缀匹配),空为清空', current);
                if (updated !== null) {
                    prefs.siteWhitelist = updated;
                    savePrefs(prefs);
                    alert('白名单已保存');
                }
            }
        );

        registerCommand(
            '重置按钮位置',
            function resetButtonPosition() {
                prefs.pos = null;
                savePrefs(prefs);
                const host = document.getElementById(HOST_ID);
                if (host) {
                    host.remove();
                }
                ensureUI();
                alert('按钮位置已重置');
            }
        );

        registerCommand(
            '强密码设置',
            function openSettingsMenu() {
                ensureUI();
                if (typeof window.__gm_sp_openSettings === 'function') {
                    window.__gm_sp_openSettings();
                }
            }
        );
    }

    // ========== 密码生成 ==========
    function randomBytes(length) {
        const arr = new Uint8Array(length);
        crypto.getRandomValues(arr);
        return arr;
    }

    function generatePassword(len = 16, opts = {}) {
        const options = Object.assign({}, prefs, opts);
        const charSets = [];

        if (options.useLower) charSets.push('abcdefghijklmnopqrstuvwxyz');
        if (options.useUpper) charSets.push('ABCDEFGHIJKLMNOPQRSTUVWXYZ');
        if (options.useNumbers) charSets.push('0123456789');
        if (options.useSymbols) charSets.push('!@#$%^&*()-_=+[]{};:,.<>/?');

        const allChars = charSets.join('');
        if (!allChars) {
            return '';
        }

        const similarChars = new Set(['0', 'O', 'o', 'I', 'l', '1']);
        const availableChars = options.excludeSimilar
            ? [...allChars].filter(char => !similarChars.has(char))
            : [...allChars];

        let password = '';
        const randomValues = randomBytes(len);
        for (let i = 0; i < len; i++) {
            password += availableChars[randomValues[i] % availableChars.length];
        }
        return password;
    }

    // ========== 剪贴板与输入框操作 ==========
    async function copyToClipboard(text) {
        // 方法 1: 使用 navigator.clipboard API
        if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
            try {
                await navigator.clipboard.writeText(text);
                return true;
            } catch (error) {
                // 尝试备选方案
            }
        }

        // 方法 2: 使用 Tampermonkey 的 GM_setClipboard
        if (typeof GM_setClipboard === 'function') {
            try {
                GM_setClipboard(text);
                return true;
            } catch (error) {
                console.warn('GM_setClipboard 失败', error);
            }
        }

        // 方法 3: 回退到 textarea + execCommand
        try {
            const textarea = document.createElement('textarea');
            textarea.value = text;
            textarea.style.position = 'fixed';
            textarea.style.left = '-9999px';
            document.body.appendChild(textarea);
            textarea.select();
            document.execCommand('copy');
            document.body.removeChild(textarea);
            return true;
        } catch (error) {
            console.warn('复制失败', error);
        }

        return false;
    }

    function injectToField(field, value) {
        if (!field) {
            return false;
        }

        const tagName = field.tagName.toLowerCase();
        if (tagName === 'input' || tagName === 'textarea') {
            field.focus();
            field.value = value;
            triggerInput(field);
            return true;
        }

        if (field.isContentEditable) {
            field.focus();
            field.innerText = value;
            triggerInput(field);
            return true;
        }

        return false;
    }

    function triggerInput(element) {
        element.dispatchEvent(new Event('input', { bubbles: true }));
        element.dispatchEvent(new Event('change', { bubbles: true }));
    }

    function isInputField(element) {
        const tagName = element.tagName.toLowerCase();
        return tagName === 'input' || tagName === 'textarea' || element.isContentEditable;
    }

    function guessTargets() {
        const selector = 'input[type="password"], input, textarea, [contenteditable="true"]';
        const allTargets = Array.from(document.querySelectorAll(selector));

        const focused = document.activeElement;
        if (focused && isInputField(focused)) {
            const otherTargets = allTargets.filter(target => target !== focused);
            return [focused, ...otherTargets];
        }

        return allTargets;
    }

    function showNotification(message) {
        if (typeof window.__gm_sp_showToast === 'function') {
            window.__gm_sp_showToast(message);
        }
    }

    function attemptAutoInject(password) {
        const targets = guessTargets();
        const passwordField = targets.find(field => field.type === 'password');
        const targetField = passwordField || targets[0];

        if (!targetField) {
            showNotification('未找到可注入的输入框');
            return;
        }

        const success = injectToField(targetField, password);
        showNotification(success ? '已注入密码' : '注入失败');
    }

    // ========== UI 创建 ==========
    function ensureUI() {
        if (document.getElementById(HOST_ID)) {
            return;
        }

        const host = document.createElement('div');
        host.id = HOST_ID;
        host.style.position = 'fixed';
        host.style.right = '0';
        host.style.bottom = '0';
        host.style.zIndex = '2147483647';
        host.style.pointerEvents = 'none';

        const parent = (document.body && getComputedStyle(document.body).transform === 'none')
            ? document.body
            : document.documentElement;
        parent.appendChild(host);
        const shadow = host.attachShadow({ mode: 'closed' });

        // 插入样式
        const style = document.createElement('style');
        style.textContent = getUIStylesheet();
        shadow.appendChild(style);

        // 创建按钮和 toast 元素
        const btn = document.createElement('div');
        btn.className = 'fp-btn';
        btn.setAttribute('role', 'button');
        btn.setAttribute('aria-label', '强密码');
        btn.tabIndex = 0;
        btn.textContent = '密';
        btn.style.userSelect = 'none';
        btn.style.webkitTapHighlightColor = 'transparent';
        btn.style.boxSizing = 'border-box';
        btn.style.transform = 'translateZ(0)';
        btn.style.touchAction = 'none';

        const toast = document.createElement('div');
        toast.className = 'fp-toast';

        shadow.appendChild(btn);
        shadow.appendChild(toast);

        // 恢复已保存位置
        if (prefs.pos && typeof prefs.pos.left === 'number' && typeof prefs.pos.top === 'number') {
            btn.style.left = prefs.pos.left + 'px';
            btn.style.top = prefs.pos.top + 'px';
            btn.style.right = 'auto';
            btn.style.bottom = 'auto';
        }

        // Toast 提示函数
        function showToast(message) {
            toast.textContent = message;
            toast.style.display = 'block';
            setTimeout(function hideToast() {
                toast.style.display = 'none';
            }, TOAST_DURATION);
        }
        window.__gm_sp_showToast = showToast;

        // 按钮交互处理
        btn.addEventListener('pointerdown', createPointerDownHandler());
        btn.addEventListener('dblclick', createDoubleClickHandler());

        function createPointerDownHandler() {
            return function handlePointerDown(startEvent) {
                if (startEvent.button !== 0) {
                    return;
                }
                startEvent.preventDefault();
                startEvent.stopPropagation();

                btn.classList.add('dragging');
                btn.style.transform = 'none';
                btn.style.transition = 'none';

                if (btn.setPointerCapture) {
                    btn.setPointerCapture(startEvent.pointerId);
                }

                const startX = startEvent.clientX;
                const startY = startEvent.clientY;
                const rect = btn.getBoundingClientRect();
                const startLeft = rect.left;
                const startTop = rect.top;
                let moved = false;

                function handlePointerMove(moveEvent) {
                    const deltaX = moveEvent.clientX - startX;
                    const deltaY = moveEvent.clientY - startY;
                    const distance = Math.hypot(deltaX, deltaY);

                    if (!moved && distance < DRAG_THRESHOLD) {
                        return;
                    }
                    moved = true;
                    btn.style.left = (startLeft + deltaX) + 'px';
                    btn.style.top = (startTop + deltaY) + 'px';
                    btn.style.right = 'auto';
                    btn.style.bottom = 'auto';
                }

                function handlePointerUp(endEvent) {
                    if (btn.releasePointerCapture) {
                        btn.releasePointerCapture(startEvent.pointerId);
                    }
                    document.removeEventListener('pointermove', handlePointerMove);
                    document.removeEventListener('pointerup', handlePointerUp);

                    btn.classList.remove('dragging');
                    btn.style.transition = '';
                    btn.style.transform = '';

                    if (moved) {
                        const finalRect = btn.getBoundingClientRect();
                        prefs.pos = { left: finalRect.left, top: finalRect.top };
                        savePrefs(prefs);
                    } else {
                        handleButtonClick(endEvent.shiftKey);
                    }
                }

                document.addEventListener('pointermove', handlePointerMove);
                document.addEventListener('pointerup', handlePointerUp);
            };
        }

        async function handleButtonClick(shiftKey) {
            const password = generatePassword(prefs.length, {});
            const success = await copyToClipboard(password);
            lastGenerated = password;

            if (success) {
                showToast('已复制密码');
            } else {
                showToast('复制失败');
            }

            if (shiftKey || prefs.autoInject) {
                attemptAutoInject(password);
            }
        }

        function createDoubleClickHandler() {
            return function handleDoubleClick(event) {
                event.stopPropagation();
                event.preventDefault();
                openSettings();
            };
        }

        window.__gm_sp_openSettings = openSettings;

        function openSettings() {
            const existing = shadow.querySelector('.fp-panel');
            if (existing) {
                existing.remove();
                return;
            }

            const panel = document.createElement('div');
            panel.className = 'fp-panel';
            panel.setAttribute('tabindex', '-1');
            panel.innerHTML = createSettingsPanelHTML();
            shadow.appendChild(panel);

            setTimeout(function focusPanel() {
                panel.focus();
            }, 10);

            panel.querySelector('#sp-close').addEventListener('click', function closePanel() {
                panel.remove();
            });
            panel.querySelector('#sp-save').addEventListener('click', function saveSettings() {
                updatePrefsFromPanel(panel);
                savePrefs(prefs);
                showToast('设置已保存');
                panel.remove();
            });
        }

        function createSettingsPanelHTML() {
            const checked = function (value) { return value ? 'checked' : ''; };
            return `
                <div style="font-weight:600;margin-bottom:8px">强密码设置</div>
                <div style="display:flex;gap:8px;margin-bottom:8px">
                    <label>长度<input type="number" min="4" max="128" value="${prefs.length}" id="sp-length"></label>
                </div>
                <div style="display:flex;gap:8px;margin-bottom:8px;flex-wrap:wrap">
                    <label><input type="checkbox" id="sp-lower" ${checked(prefs.useLower)}> 小写</label>
                    <label><input type="checkbox" id="sp-upper" ${checked(prefs.useUpper)}> 大写</label>
                    <label><input type="checkbox" id="sp-num" ${checked(prefs.useNumbers)}> 数字</label>
                    <label><input type="checkbox" id="sp-sym" ${checked(prefs.useSymbols)}> 符号</label>
                </div>
                <div style="display:flex;gap:8px;margin-bottom:8px;flex-wrap:wrap">
                    <label><input type="checkbox" id="sp-ex" ${checked(prefs.excludeSimilar)}> 排除相似</label>
                    <label><input type="checkbox" id="sp-auto" ${checked(prefs.autoInject)}> Shift+点击注入</label>
                </div>
                <div style="display:flex;gap:8px;justify-content:flex-end">
                    <button id="sp-save">保存</button>
                    <button id="sp-close">关闭</button>
                </div>
            `;
        }

        function updatePrefsFromPanel(panel) {
            const length = Number(panel.querySelector('#sp-length').value);
            prefs.length = Math.max(4, Math.min(128, length || prefs.length));
            prefs.useLower = panel.querySelector('#sp-lower').checked;
            prefs.useUpper = panel.querySelector('#sp-upper').checked;
            prefs.useNumbers = panel.querySelector('#sp-num').checked;
            prefs.useSymbols = panel.querySelector('#sp-sym').checked;
            prefs.excludeSimilar = panel.querySelector('#sp-ex').checked;
            prefs.autoInject = panel.querySelector('#sp-auto').checked;
        }
    }

    function getUIStylesheet() {
        return `
            .fp-btn {
                position: fixed !important;
                right: 12px !important;
                bottom: 12px !important;
                z-index: 2147483648 !important;
                width: 36px;
                height: 36px;
                padding: 0;
                border-radius: 50%;
                background: #1e88e5;
                color: #fff;
                border: 1px solid rgba(255, 255, 255, 0.06);
                font-size: 11px;
                font-weight: 600;
                cursor: pointer;
                box-shadow: 0 6px 14px rgba(0, 0, 0, 0.08);
                opacity: 0.95;
                transition: transform 0.14s ease, opacity 0.14s ease;
                pointer-events: auto;
                display: flex;
                align-items: center;
                justify-content: center;
            }
            .fp-btn:hover {
                transform: translateY(-2px) scale(1.035);
                opacity: 1;
            }
            .fp-btn:focus {
                outline: none;
                box-shadow: 0 0 0 4px rgba(30, 136, 229, 0.12);
            }
            .fp-btn.dragging {
                transform: none !important;
                transition: none !important;
            }
            .fp-toast {
                position: fixed !important;
                right: 12px !important;
                bottom: 56px !important;
                z-index: 2147483648 !important;
                padding: 8px 10px;
                background: rgba(0, 0, 0, 0.8);
                color: #fff;
                border-radius: 4px;
                font-size: 12px;
                display: none;
                pointer-events: none;
            }
            .fp-panel {
                position: fixed !important;
                right: 12px !important;
                bottom: 56px !important;
                left: auto !important;
                top: auto !important;
                z-index: 2147483649 !important;
                width: 320px;
                padding: 12px;
                background: #fff;
                border: 1px solid #ddd;
                border-radius: 6px;
                box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
                font-family: system-ui, Segoe UI, Arial;
                margin: 0 !important;
                pointer-events: auto;
            }
        `;
    }

    // ========== 初始化 ==========
    window.addEventListener('load', function initializeScript() {
        addMenuCommands();
        if (shouldShowUI()) {
            ensureUI();
        } else {
            observeForPasswordFields();
        }
    });
})();