吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 2488|回复: 36
收起左侧

[其他原创] 【油猴脚本】AI时代程序员必备神器 - Prompt 优化,中英翻译

[复制链接]
PJlay 发表于 2026-5-21 17:51
本帖最后由 PJlay 于 2026-5-21 22:55 编辑

大家好,我是你们的数字老伙计。今天不聊虚的,直接给大家安利一个让工作效率原地起飞的神器——智语助手

你可能会问:现在 AI 工具那么多,你这又是啥新花样?
这么说吧。如果你曾经:

  • 对着 ChatGPT 憋了十分钟,只憋出一句“帮我写个方案”;
  • 选中一段中文想翻译成英文,结果还要切到翻译网站粘贴;
  • 想让 AI 把一段话润色成小红书风格,但不知道怎么描述;
  • 甚至,你连 Prompt 怎么写都头疼……

那这个油猴脚本,可能会成为你的「嘴替」

1. 这是个什么玩意儿?

简单说,智语助手是一个浏览器里的万能文本优化工具。它以油猴脚本(Tampermonkey)的形式存在,轻轻一个悬浮球 ,就能在任何网页上召唤出 AI 面板。

它的核心本事就三样:
  • 把一句人话,扩写成一条高质量 AI 提示词(Prompt 增强)
  • 中英互译,地道到像母语者(不再有“翻译腔”)
  • 各种风格改写,全自动(朋友圈文案、邮件润色、学术腔调…)
而且——无需复制粘贴、不挑网站、不挑输入框。你在哪儿打字,它就能在哪儿帮你优化。

脚本界面

脚本界面


2. 为什么需要它?

咱们来几个真实场景感受一下:
场景一:
你和 AI 聊天,但你的 Prompt 只有一句……
:帮我写个直播带货脚本。
智语助手(提示词增强)
你是一名经验丰富的直播带货主播,请为以下产品设计一段 3 分钟的直播口播脚本:产品是“便携式挂烫机”,定价 199 元,核心卖点是一键出汽、30 秒速热、可折叠。脚本需包含开场吸引注意、痛点展示、产品演示、限时优惠、促单结尾,语气热情亲切,每段配上适当的表情和动作提示。

就问你,哪个 Prompt 更能让 AI 输出你想要的东西?

场景二:
老板让你把中文周报翻成英文
原文:“本周我们完成了Q3规划初稿,并与跨部门团队对齐了关键里程碑。”

智语助手 - 译为英文
*This week we finalized the initial draft of the Q3 roadmap and aligned on key milestones with cross-functional teams.*

场景三:
你选中一段话,一个气泡就飘上来了
在任何网页上,只要选中文字(≥1 个字),一个黑色小气泡会自动浮现,上面有三个按钮:增强、 译英、译中。

点一下,面板打开,优化开始,整个过程不超过 5 秒。这才是真正的“即选即用”,不用去另一个页面,不用离开你的工作流。

划词翻译

划词翻译


3. 怎么装?三步搞定

  • 装个油猴插件(Tampermonkey),Chrome/Edge/Firefox 商店都有,搜一下就行。
  • 点击插件图标 → 创建新脚本,把文章结尾的完整代码粘贴进去,保存。
  • 刷新任意页面,你会看到一个漂亮的紫色悬浮球出现在屏幕右侧。

就这么简单,不需要配置服务器,不需要装任何软件。
(假装有截图:一个紫色渐变悬浮球,鼠标悬停时有放大的光晕效果)

4. 怎么用?保姆级指南

初次设置:配好你的 API

点一下悬浮球打开面板 → 点 设置按钮 → 填入你的 API Key(支持 DeepSeek、OpenAI、Moonshot、Qwen、Gemini,甚至本地 Ollama)。
如果你还没 API Key,去任意官网注册一个。(示例:DeepSeek 开放平台)

面板里可以直接切换服务商,所有设置自动保存(API Key 本地安全保存)。
四种模式,一键切换
面板上方有四个模式按钮:

  • 提示词增强 —— 把碎碎念变成精装 Prompt
  • 译为英文 —— 意译,地道,没有 Chinese English
  • 译为中文 —— 意译,符合中文阅读节奏
  • 自定义 —— 想怎么玩就怎么玩(例如“改写为武侠小说风格”)
(未来还会有更多)

选中模式 → 在输入框里粘贴或输入文本 → 点  运行,AI 就开始工作了。
输出结果会显示在下方,你可以:
  • 复制 到剪贴板
  • 应用 到刚才的输入框里(支持直接替换你当前正在编辑的框!)

5. 它到底好在哪?

1. 提示词工程师附体
普通 AI 工具只把你的输入原样发给 API,而智语助手内置了一套极其详细的系统提示词。它会对你的原始文本做二次加工,弥补你表达上的模糊和不足,让模型真正理解你想要什么。

2. 长文本自动分块
如果你粘贴了一篇 3000 字的文章,脚本会自动把它切成合适的大小,逐块处理后再拼接。进度条会告诉你处理到哪儿了,不会让大模型因为上下文超长而崩溃。

3. 防 Prompt 注入
你可以放心地用它处理任何用户输入的内容,因为脚本会把你的文本包在 <input> 标签里,和系统指令隔离,避免有人故意在文本里写“忽略之前的指令”这种注入攻击。

4. 跨输入框“应用”
点了应用,它会智能检测你刚才在哪个输入框/文本框编辑,直接替换内容。支持原生 input、textarea,甚至 contenteditable 的富文本编辑器(比如飞书文档、Notion 等),这兼容性,我给满分。

5. 全键盘操作党狂喜
  • Ctrl+Shift+O — 打开/关闭面板
  • Ctrl+Shift+Enter — 快捷运行
  • Ctrl+Shift+H — 隐藏/显示悬浮球
  • Ctrl+Shift+D — 开关选文弹窗
  • Esc — 关闭面板或弹窗

真正的效率工具,手不用离开键盘。

6. 一些细节

  • 悬浮球可以拖:你嫌它挡地方,就按住拖到角落里。
  • 历史记录自动保存:每次优化都自动存下来,支持搜索、复制、应用、导出 JSON。
  • 选文气泡可以关:如果觉得气泡烦,在设置里把“选文弹窗”的勾去掉,世界就清净了。
  • 支持流式输出:如果你用的是支持 streaming 的 API,结果会一个字一个字蹦出来,体验跟 ChatGPT 一模一样。

    历史记录

    历史记录

7. 谁最需要它?

  • 经常和 AI 对话的内容创作者:用它生成更棒的 Prompt,让 ChatGPT 产出更好的内容。
  • 需要中英互译的职场人:不再忍受机翻的尴尬,邮件、文档一键润色。
  • 浏览器重度用户:在网页表单、在线文档、社交媒体的评论框里随时召唤 AI。
  • 讨厌在不同工具间来回切换的你。

   创作不易,如果脚本对各位大佬有用,感谢各位大佬评论一下 留个痕迹就行!!!
[JavaScript] 纯文本查看 复制代码
// ==UserScript==
// @name         智语助手
// @version      1.0
// @description  精准提示词优化、中英翻译、自定义优化、快捷键操作、历史记录查询。
// @AuThor       Lay.zhang
// @match        *://*/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_setClipboard
// @connect      *
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    const P   = 'tmo-';
    const Z   = { base: 99990, panel: 99992, modal: 99995, menu: 99998, toast: 99999, quick: 99997 };
    const SK  = { SETTINGS: 'tmo_s', HISTORY: 'tmo_h', BTN_POS: 'tmo_bp', PANEL_POS: 'tmo_pp', HIST_POS: 'tmo_hp', BTN_VIS: 'tmo_bv' };
    const MAX_HIST            = 100;
    const MAX_CHARS_PER_CHUNK = 1500;

    /* ─── API 预设 ──────────────────────────────────────────────── */
    const API_PRESETS = [
        { name: 'DeepSeek',  url: 'https://api.deepseek.com/v1',                             model: 'deepseek-chat'        },
        { name: 'OpenAI',    url: 'https://api.openai.com/v1',                               model: 'gpt-4o-mini'          },
        { name: 'Moonshot',  url: 'https://api.moonshot.cn/v1',                              model: 'moonshot-v1-8k'       },
        { name: 'Qwen',      url: 'https://dashscope.aliyuncs.com/compatible-mode/v1',       model: 'qwen-plus'            },
        { name: 'Gemini',    url: 'https://generativelanguage.googleapis.com/v1beta/openai', model: 'gemini-2.0-flash'     },
        { name: 'Ollama',    url: 'http://localhost:11434/v1',                               model: 'llama3'               },
    ];

    const DEFAULT_SETTINGS = {
        apiBaseUrl:   'https://api.deepseek.com/v1',
        apiKey:       '',
        model:        'deepseek-chat',
        temperature:  0.7,
        maxTokens:    2000,
        systemPrompt: `你是一名顶级提示词工程师(Prompt Engineer),同时精通中英文写作与文字优化。
你的职责是根据用户指定的【优化模式】,对输入文本进行精确处理。

核心规范:
1. 严格遵守每种模式的输出要求,不得混用模式规则
2. 只输出处理后的最终文本,禁止添加任何解释、序号前缀、markdown标题或多余标注
3. 保持原文意图不变,除非模式明确要求扩展或改变风格
4. 输出语言与原文一致(翻译模式除外)`,
        streaming:    false,
        selBubble:    true,
        autoRetry:    false,
        maxRetries:   3,
    };

    /* ═══════════════════════════════════════════════════════════════
       风格定义(增强 & 翻译)
    ═══════════════════════════════════════════════════════════════ */
    const STYLES = [
        {
            id: 'enhance',
            name: '&#128640; 提示词增强',
            temperature: 0.7,
            prompt: `【模式:提示词增强】
你将扮演提示词工程师,把用户输入的简短或模糊描述,扩展为一条结构完整、指令清晰、可直接投入使用的高质量 AI 提示词。

操作步骤:
① 识别用户的核心意图与目标受众
② 使用精准的动词指令替换模糊表达,不要过于冗余
③ 合并为一段流畅、完整的提示词,不要修改原有格式,不加任何标注或分项说明
现在请处理用户输入,直接输出优化后的提示词,不要有任何前缀或解释。`,
        },
        {
            id: 'trans_en',
            name: '&#127760; 译为英文',
            temperature: 0.3,
            prompt: `【模式:中译英】
将原文翻译成专业、地道的英文,遵循以下原则:

① 意译优先:不逐字硬译,根据英文表达习惯重组句子结构
② 词汇选择:优先使用母语级英文用词,避免"Chinese English"(如不用 "very very" "do" 动词滥用等)
③ 语气匹配:原文正式则译文正式,原文口语则译文口语
④ 专名处理:人名/地名保留拼音或使用通行英译;技术术语使用业界标准译法
⑤ 文化转换:涉及中文特有表达时,用对等的英文习语或加简短括号说明

只输出英文译文,不附中文,不加注释。`,
        },
        {
            id: 'trans_zh',
            name: '&#127760; 译为中文',
            temperature: 0.3,
            prompt: `【模式:英译中】
将原文翻译成流畅、自然的现代中文,遵循以下原则:

① 意译优先:不逐词直译,根据中文表达习惯重组语序,避免"翻译腔"
② 词汇选择:优先使用中文母语习惯的词汇,避免生硬的音译(有约定俗成译法的专名除外)
③ 语气匹配:正式原文译为书面中文;口语原文译为自然白话
④ 长句处理:适当拆分英文长句,符合中文"短句多、断句灵活"的节奏
⑤ 文化转换:西方文化特有概念用中文读者熟悉的对应表达,或加简短括号注释

只输出中文译文,不附英文,不加注释。`,
        },
        { id: 'custom', name: '&#9881; 自定义', temperature: null, prompt: '', isCustom: true },
    ];

    /* ═══════════════════════════════════════════════════════════════
       持久化存储
    ═══════════════════════════════════════════════════════════════ */
    const store = {
        get(k, def = null) {
            try {
                const v = GM_getValue(k, null);
                if (v === null || v === undefined) return def;
                if (typeof v === 'string') { try { return JSON.parse(v); } catch { return def; } }
                return v;
            } catch { return def; }
        },
        set(k, v) {
            try { GM_setValue(k, JSON.stringify(v)); }
            catch { try { localStorage.setItem(k, JSON.stringify(v)); } catch {} }
        },
    };

    const getSettings = () => ({ ...DEFAULT_SETTINGS, ...store.get(SK.SETTINGS, {}) });
    const saveSettings = s  => store.set(SK.SETTINGS, s);
    const getHistory   = () => { const h = store.get(SK.HISTORY, []); return Array.isArray(h) ? h : []; };
    const saveHistory  = h  => store.set(SK.HISTORY, h.slice(0, MAX_HIST));

    /* ═══════════════════════════════════════════════════════════════
       Toast 通知
    ═══════════════════════════════════════════════════════════════ */
    function showToast(msg, type = 'info', ms = 2500) {
        document.querySelector(`.${P}toast`)?.remove();
        const el = document.createElement('div');
        el.className = `${P}toast`;
        const bg = { success: '#10b981', error: '#ef4444', info: '#6366f1', warning: '#f59e0b' }[type] ?? '#6366f1';
        Object.assign(el.style, {
            position: 'fixed', top: '18px', left: '50%', transform: 'translateX(-50%)',
            zIndex: Z.toast, background: bg, color: '#fff', padding: '9px 18px',
            borderRadius: '8px', fontSize: '13px', fontFamily: 'system-ui,sans-serif',
            boxShadow: '0 6px 20px rgba(0,0,0,.22)', whiteSpace: 'nowrap', pointerEvents: 'none',
            animation: `${P}fadeDown .25s ease`,
        });
        el.textContent = msg;
        document.body.appendChild(el);
        setTimeout(() => { el.style.transition = 'opacity .3s'; el.style.opacity = '0'; setTimeout(() => el.remove(), 300); }, ms);
    }

    /* ═══════════════════════════════════════════════════════════════
       工具函数
    ═══════════════════════════════════════════════════════════════ */
    const esc = s => String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');

    function textStats(s) {
        const chars = s.length;
        const words = s.trim() ? s.trim().split(/\s+/).length : 0;
        return chars > 0 ? `${chars}字${words > 1 ? ` ${words}词` : ''}` : '0字';
    }

    const TEXT_INPUT_TYPES = new Set(['text','search','url','email','tel','password','number','']);
    function isTextEl(el) {
        if (!el) return false;
        if (el.tagName === 'TEXTAREA') return true;
        if (el.tagName === 'INPUT') return TEXT_INPUT_TYPES.has((el.type || '').toLowerCase());
        if (el.isContentEditable) return true;
        return false;
    }

    function splitIntoChunks(text, maxLen = MAX_CHARS_PER_CHUNK) {
        if (text.length <= maxLen) return [text];
        const chunks = [];
        const paragraphs = text.split(/\n{2,}/);
        let buf = '';
        for (const para of paragraphs) {
            if (buf.length + para.length + 2 > maxLen) {
                if (buf) { chunks.push(buf.trim()); buf = ''; }
                if (para.length > maxLen) {
                    const sentences = para.split(/(?<=[。!?.!?])/);
                    for (const sent of sentences) {
                        if (buf.length + sent.length > maxLen) {
                            if (buf) { chunks.push(buf.trim()); buf = ''; }
                            if (sent.length > maxLen) {
                                for (let i = 0; i < sent.length; i += maxLen) chunks.push(sent.slice(i, i + maxLen));
                            } else { buf = sent; }
                        } else { buf += sent; }
                    }
                } else { buf = para; }
            } else {
                buf += (buf ? '\n\n' : '') + para;
            }
        }
        if (buf.trim()) chunks.push(buf.trim());
        return chunks;
    }

    function makeDraggable(el, handle, { exclude = null, onEnd = null } = {}) {
        let dragging = false, ox = 0, oy = 0;
        const start = (cx, cy) => {
            dragging = true;
            const r = el.getBoundingClientRect();
            ox = cx - r.left; oy = cy - r.top;
            el.style.transition = 'none';
        };
        const move = (cx, cy) => {
            if (!dragging) return;
            const x = Math.max(0, Math.min(window.innerWidth  - el.offsetWidth,  cx - ox));
            const y = Math.max(0, Math.min(window.innerHeight - el.offsetHeight, cy - oy));
            el.style.left = x + 'px'; el.style.top = y + 'px';
        };
        const end = () => {
            if (!dragging) return;
            dragging = false;
            el.style.transition = '';
            onEnd?.({ x: parseInt(el.style.left) || 0, y: parseInt(el.style.top) || 0 });
        };
        handle.addEventListener('mousedown', e => {
            if (e.button !== 0) return;
            if (exclude && e.target.closest(exclude)) return;
            e.preventDefault(); start(e.clientX, e.clientY);
        });
        handle.addEventListener('touchstart', e => {
            if (e.touches.length !== 1) return;
            if (exclude && e.target.closest(exclude)) return;
            e.preventDefault(); start(e.touches[0].clientX, e.touches[0].clientY);
        }, { passive: false });
        document.addEventListener('mousemove', e => move(e.clientX, e.clientY));
        document.addEventListener('touchmove', e => {
            if (dragging) { e.preventDefault(); move(e.touches[0].clientX, e.touches[0].clientY); }
        }, { passive: false });
        document.addEventListener('mouseup',  end);
        document.addEventListener('touchend', end);
    }

    /* ═══════════════════════════════════════════════════════════════
       主类 TextOptimizer
    ═══════════════════════════════════════════════════════════════ */
    class TextOptimizer {
        constructor() {
            this.cfg      = getSettings();
            this.history  = getHistory();
            this.open     = false;
            this.loading  = false;
            this.srcEl    = null;
            this.srcSel   = null;
            this.style    = 'enhance';
            this.abort    = null;
            this.btnVis   = store.get(SK.BTN_VIS, true) !== false;
            this.btnPos   = store.get(SK.BTN_POS, null) || { x: window.innerWidth - 70,  y: window.innerHeight / 2 - 25  };
            this.panelPos = store.get(SK.PANEL_POS, null) || { x: Math.max(20, window.innerWidth - 450), y: Math.max(20, window.innerHeight / 2 - 300) };
            this._draggingBtn = false;
            this._btnOffset   = { x: 0, y: 0 };
            this._bubbleText  = '';
            this._chunkTotal  = 0;
            this._chunkDone   = 0;
            this.lastSelectionRange = null;
            this._lastQuickOriginal = '';
            this._lastQuickTargetLang = '';
            this.quickPanel = null;
            this.activeQuickAbort = null;
            this._bubbleVisible = false;
            this.init();
        }

        /* ── 初始化 ─────────────────────────────────────────────── */
        init() {
            this.injectCSS();
            this.createBtn();
            this.createPanel();
            this.createBubble();
            this.createHistoryModal();
            this.createQuickTranslatePanel();
            this.bindKeys();
        }

        /* ── 注入样式(保持原有)────────────────────────────────── */
        injectCSS() {
            GM_addStyle(`
            @keyframes ${P}fadeDown  { from{opacity:0;transform:translateX(-50%) translateY(-16px);}to{opacity:1;transform:translateX(-50%) translateY(0);} }
            @keyframes ${P}fadeIn    { from{opacity:0;transform:scale(.93);}to{opacity:1;transform:scale(1);} }
            @keyframes ${P}pulse     { 0%,100%{box-shadow:0 0 0 0 rgba(99,102,241,.5);}50%{box-shadow:0 0 0 11px rgba(99,102,241,0);} }
            @keyframes ${P}spin      { to{transform:rotate(360deg);} }
            @keyframes ${P}bubblePop { from{opacity:0;transform:translateY(5px) scale(.94);}to{opacity:1;transform:none;} }

            .${P}btn{position:fixed;width:46px;height:46px;border-radius:50%;
                background:linear-gradient(135deg,#667eea,#764ba2);cursor:pointer;
                z-index:${Z.base};display:flex;align-items:center;justify-content:center;
                font-size:22px;box-shadow:0 4px 16px rgba(102,126,234,.45);
                user-select:none;border:2px solid rgba(255,255,255,.22);
                animation:${P}pulse 2.5s infinite;transition:transform .2s,box-shadow .2s;}
            .${P}btn:hover{transform:scale(1.1);box-shadow:0 8px 28px rgba(102,126,234,.6);}
            .${P}btn.drag{animation:none;cursor:grabbing;box-shadow:0 12px 32px rgba(0,0,0,.3);}

            .${P}panel{position:fixed;z-index:${Z.panel};width:430px;
                max-width:calc(100vw - 12px);max-height:88vh;
                background:#fff;border-radius:16px;
                box-shadow:0 20px 60px rgba(0,0,0,.2),0 0 0 1px rgba(0,0,0,.05);
                font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif;
                display:none;flex-direction:column;overflow:hidden;
                animation:${P}fadeIn .22s ease;}
            .${P}panel.open{display:flex;}

            .${P}ph{display:flex;align-items:center;padding:11px 14px;
                background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;
                cursor:move;user-select:none;gap:8px;flex-shrink:0;
                border-radius:16px 16px 0 0;}
            .${P}ph-title{flex:1;font-weight:600;font-size:14px;}
            .${P}hbtn{width:28px;height:28px;border-radius:50%;border:none;
                background:rgba(255,255,255,.22);color:#fff;cursor:pointer;
                font-size:13px;display:flex;align-items:center;justify-content:center;
                padding:0;transition:background .18s;flex-shrink:0;}
            .${P}hbtn:hover{background:rgba(255,255,255,.38);}

            .${P}body{flex:1;overflow-y:auto;padding:12px 14px;
                display:flex;flex-direction:column;gap:9px;scrollbar-width:thin;}
            .${P}body::-webkit-scrollbar{width:4px;}
            .${P}body::-webkit-scrollbar-thumb{background:#d1d5db;border-radius:2px;}

            .${P}settings{display:none;flex-direction:column;gap:8px;
                padding:10px 12px;background:#f8fafc;border-radius:10px;
                border:1px solid #e2e8f0;}
            .${P}settings.show{display:flex;}
            .${P}set-label{font-size:10px;font-weight:700;color:#64748b;
                text-transform:uppercase;letter-spacing:.7px;}
            .${P}presets{display:flex;flex-wrap:wrap;gap:5px;}
            .${P}preset{padding:4px 10px;border-radius:10px;border:1px solid #e2e8f0;
                background:#fff;cursor:pointer;font-size:11px;font-weight:500;
                color:#475569;transition:all .15s;}
            .${P}preset:hover{border-color:#667eea;color:#667eea;background:#f5f3ff;}
            .${P}set-row{display:flex;align-items:center;gap:8px;font-size:12px;color:#475569;}
            .${P}set-row label{min-width:58px;font-weight:500;}
            .${P}set-inp{flex:1;padding:5px 8px;border:1px solid #e2e8f0;
                border-radius:6px;font-size:12px;outline:none;background:#fff;}
            .${P}set-inp:focus{border-color:#667eea;}
            .${P}set-ta{width:100%;min-height:46px;padding:5px 8px;font-size:11px;
                border:1px solid #e2e8f0;border-radius:6px;outline:none;font-family:inherit;
                resize:vertical;background:#fff;box-sizing:border-box;}
            .${P}set-ta:focus{border-color:#667eea;}
            .${P}check-row{display:flex;flex-wrap:wrap;gap:12px;align-items:center;}
            .${P}check-row label{display:flex;align-items:center;gap:4px;
                font-size:12px;color:#475569;cursor:pointer;user-select:none;}

            .${P}lbl{font-size:10px;font-weight:700;color:#64748b;
                text-transform:uppercase;letter-spacing:.7px;}
            .${P}tags{display:flex;flex-wrap:wrap;gap:5px;}
            .${P}tag{padding:5px 11px;border-radius:20px;border:1.5px solid #e2e8f0;
                background:#fff;cursor:pointer;font-size:12px;font-weight:500;
                color:#374151;transition:all .16s;user-select:none;}
            .${P}tag:hover{border-color:#667eea;background:#f5f3ff;}
            .${P}tag.on{border-color:#667eea;background:#667eea;color:#fff;}

            .${P}custom{display:none;flex-direction:column;gap:4px;}
            .${P}custom.show{display:flex;}
            .${P}custom textarea{width:100%;min-height:56px;padding:7px 9px;
                border:1.5px solid #e2e8f0;border-radius:8px;font-size:12px;
                font-family:inherit;resize:vertical;outline:none;box-sizing:border-box;}
            .${P}custom textarea:focus{border-color:#667eea;}

            .${P}taw{position:relative;}
            .${P}ta{width:100%;min-height:88px;max-height:200px;
                padding:8px 26px 18px 10px;border:1.5px solid #e2e8f0;border-radius:10px;
                font-size:13px;font-family:inherit;resize:vertical;outline:none;
                transition:border-color .2s,background .2s;box-sizing:border-box;
                background:#f8fafc;overflow-y:auto;}
            .${P}ta:focus{border-color:#667eea;background:#fff;}
            .${P}ta.out{background:#f0fdf4;border-color:#86efac;cursor:default;}
            .${P}ta.stream{border-color:#667eea;background:#fafaff;}
            .${P}ta.err{border-color:#fca5a5;background:#fef2f2;}
            .${P}ta-cnt{position:absolute;bottom:5px;right:8px;
                font-size:10px;color:#9ca3af;pointer-events:none;user-select:none;}
            .${P}ta-x{position:absolute;top:6px;right:6px;width:17px;height:17px;
                border-radius:50%;border:none;background:#e5e7eb;cursor:pointer;
                font-size:10px;color:#6b7280;display:none;align-items:center;
                justify-content:center;padding:0;line-height:1;transition:background .15s;}
            .${P}ta-x:hover{background:#d1d5db;color:#111;}
            .${P}taw:hover .${P}ta-x{display:flex;}

            .${P}progress{display:none;flex-direction:column;gap:4px;
                background:#f0f9ff;border:1px solid #bae6fd;border-radius:8px;padding:8px 10px;}
            .${P}progress.show{display:flex;}
            .${P}progress-label{font-size:11px;color:#0369a1;font-weight:500;}
            .${P}progress-bar{height:4px;background:#e0f2fe;border-radius:2px;overflow:hidden;}
            .${P}progress-fill{height:100%;background:linear-gradient(90deg,#667eea,#764ba2);
                border-radius:2px;transition:width .3s ease;}

            .${P}row{display:flex;gap:7px;flex-wrap:wrap;}
            .${P}bp{flex:1;min-width:80px;padding:9px 14px;border-radius:9px;border:none;
                cursor:pointer;font-size:12px;font-weight:600;
                background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;
                display:flex;align-items:center;justify-content:center;gap:5px;
                transition:all .2s;}
            .${P}bp:hover:not(:disabled){transform:translateY(-1px);box-shadow:0 4px 14px rgba(102,126,234,.4);}
            .${P}bp:disabled{opacity:.6;cursor:not-allowed;}
            .${P}bs{padding:9px 12px;border-radius:9px;border:1.5px solid #e2e8f0;
                cursor:pointer;font-size:12px;font-weight:500;background:#fff;
                color:#374151;display:flex;align-items:center;justify-content:center;
                gap:4px;transition:all .16s;}
            .${P}bs:hover{border-color:#667eea;color:#667eea;background:#f5f3ff;}
            .${P}bs.danger{border-color:#fca5a5;color:#b91c1c;}
            .${P}bs.danger:hover{background:#fef2f2;}
            .${P}spin{display:inline-block;width:13px;height:13px;border-radius:50%;
                border:2px solid rgba(255,255,255,.3);border-top-color:#fff;
                animation:${P}spin .7s linear infinite;flex-shrink:0;}

            .${P}bubble{position:fixed;z-index:${Z.menu};background:#1e293b;
                border-radius:9px;padding:5px 7px;display:none;gap:3px;
                box-shadow:0 8px 24px rgba(0,0,0,.3);animation:${P}bubblePop .16s ease;}
            .${P}bubble::after{content:'';position:absolute;bottom:-5px;left:50%;
                transform:translateX(-50%);border:5px solid transparent;
                border-bottom:none;border-top-color:#1e293b;}
            .${P}bbtn{padding:4px 9px;border-radius:5px;border:none;
                background:transparent;color:#e2e8f0;cursor:pointer;
                font-size:12px;font-weight:500;transition:background .15s;white-space:nowrap;}
            .${P}bbtn:hover{background:rgba(255,255,255,.15);}
            .${P}bsep{width:1px;background:rgba(255,255,255,.18);align-self:stretch;margin:3px 0;}

            /* 快速翻译浮层 */
            .${P}quick-panel{position:fixed;z-index:${Z.quick};width:340px;
                max-width:calc(100vw - 24px);background:#fff;border-radius:12px;
                box-shadow:0 12px 32px rgba(0,0,0,.25),0 0 0 1px rgba(0,0,0,.05);
                font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
                display:none;flex-direction:column;overflow:hidden;animation:${P}fadeIn .18s ease;}
            .${P}quick-panel.show{display:flex;}
            .${P}quick-head{display:flex;align-items:center;padding:8px 12px;
                background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;
                cursor:move;user-select:none;gap:6px;font-size:12px;font-weight:500;}
            .${P}quick-head span{flex:1;}
            .${P}quick-close{background:rgba(255,255,255,.2);border:none;
                border-radius:6px;color:#fff;width:24px;height:24px;
                display:flex;align-items:center;justify-content:center;
                cursor:pointer;font-size:14px;}
            .${P}quick-body{max-height:280px;overflow-y:auto;padding:12px;
                font-size:13px;line-height:1.5;white-space:pre-wrap;
                word-break:break-word;background:#fefefe;}
            .${P}quick-loading{display:flex;align-items:center;gap:8px;
                color:#667eea;font-size:12px;padding:12px;justify-content:center;}
            .${P}quick-actions{display:flex;gap:8px;padding:8px 12px;
                border-top:1px solid #eef2f6;background:#fafcff;flex-wrap:wrap;}
            .${P}quick-actions button{flex:1;padding:6px 0;border-radius:6px;
                border:1px solid #e2e8f0;background:#fff;cursor:pointer;
                font-size:12px;transition:all .15s;}
            .${P}quick-actions button:hover{background:#f1f5f9;border-color:#667eea;color:#667eea;}
            .${P}quick-err{color:#e11d48;background:#fff0f3;padding:10px;
                border-radius:8px;font-size:12px;margin:6px;}

            .${P}hov{display:none;position:fixed;inset:0;
                background:rgba(0,0,0,.38);z-index:${Z.modal};}
            .${P}hov.open{display:block;}
            .${P}hbox{position:absolute;background:#fff;border-radius:16px;
                padding:18px 20px;width:min(720px,94vw);max-height:85vh;
                display:flex;flex-direction:column;
                box-shadow:0 20px 40px rgba(0,0,0,.3);
                font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC',sans-serif;
                transform:translate(-50%,-50%);left:50%;top:50%;}
            .${P}hhdr{display:flex;align-items:center;gap:8px;margin-bottom:10px;
                cursor:move;user-select:none;padding-bottom:10px;
                border-bottom:1px solid #f1f5f9;flex-shrink:0;}
            .${P}htitle{flex:1;font-size:15px;font-weight:600;color:#1e293b;margin:0;}
            .${P}hsearch{flex:1.4;padding:6px 10px;border:1.5px solid #e2e8f0;
                border-radius:8px;font-size:13px;outline:none;min-width:0;}
            .${P}hsearch:focus{border-color:#667eea;}
            .${P}hlist{flex:1;overflow-y:auto;display:flex;flex-direction:column;
                gap:10px;min-height:60px;scrollbar-width:thin;padding-right:2px;}
            .${P}hempty{text-align:center;color:#9ca3af;padding:24px;font-size:14px;}
            .${P}hi{background:#f8fafc;border:1px solid #e2e8f0;border-radius:10px;padding:11px;}
            .${P}hi-meta{display:flex;justify-content:space-between;align-items:center;
                margin-bottom:7px;font-size:11px;color:#64748b;}
            .${P}hi-badge{background:#667eea;color:#fff;padding:2px 8px;
                border-radius:10px;font-size:10px;font-weight:600;}
            .${P}hi-cols{display:flex;gap:8px;}
            .${P}hi-col{flex:1;background:#fff;border:1px solid #e2e8f0;
                border-radius:7px;padding:7px;font-size:11px;max-height:60px;
                overflow-y:auto;white-space:pre-wrap;word-break:break-word;color:#374151;}
            .${P}hi-acts{display:flex;gap:6px;margin-top:8px;flex-wrap:wrap;}
            .${P}hi-acts button{padding:4px 10px;border-radius:6px;border:1px solid #e2e8f0;
                background:#fff;cursor:pointer;font-size:11px;color:#475569;transition:all .15s;}
            .${P}hi-acts button:hover{border-color:#667eea;color:#667eea;background:#f5f3ff;}
            .${P}hi-acts button.del{color:#b91c1c;}
            .${P}hi-acts button.del:hover{border-color:#fca5a5;background:#fef2f2;}
            .${P}hfoot{display:flex;gap:8px;margin-top:12px;padding-top:10px;
                border-top:1px solid #f1f5f9;flex-shrink:0;}

            @media(max-width:480px){
                .${P}panel{width:calc(100vw - 8px);border-radius:12px;}
                .${P}btn{width:40px;height:40px;font-size:18px;}
                .${P}hbox{padding:14px 12px;width:96vw;}
                .${P}quick-panel{width:280px;}
            }
            `);
        }

        /* ── 悬浮球 ─────────────────────────────────────────────── */
        createBtn() {
            this.btnEl = document.createElement('div');
            this.btnEl.className = `${P}btn`;
            this.btnEl.title = '智语助手';
            this.btnEl.innerHTML = '&#10024;';
            Object.assign(this.btnEl.style, { left: this.btnPos.x + 'px', top: this.btnPos.y + 'px' });
            document.body.appendChild(this.btnEl);
            if (!this.btnVis) this.btnEl.style.display = 'none';

            let wasDragged = false, startXY = null;
            const onMove = (cx, cy) => {
                if (!this._draggingBtn) return;
                if (startXY && Math.hypot(cx - startXY.x, cy - startXY.y) > 4) wasDragged = true;
                const x = Math.max(0, Math.min(window.innerWidth  - 48, cx - this._btnOffset.x));
                const y = Math.max(0, Math.min(window.innerHeight - 48, cy - this._btnOffset.y));
                this.btnEl.style.left = x + 'px'; this.btnEl.style.top = y + 'px';
                this.btnPos = { x, y };
            };
            const onEnd = () => {
                if (!this._draggingBtn) return;
                this._draggingBtn = false;
                this.btnEl.classList.remove('drag');
                store.set(SK.BTN_POS, this.btnPos);
                setTimeout(() => { wasDragged = false; }, 60);
            };
            const onStart = (cx, cy) => {
                wasDragged = false; startXY = { x: cx, y: cy };
                const r = this.btnEl.getBoundingClientRect();
                this._btnOffset = { x: cx - r.left, y: cy - r.top };
                this._draggingBtn = true;
                this.btnEl.classList.add('drag');
            };
            this.btnEl.addEventListener('mousedown', e => { if (e.button !== 0) return; e.preventDefault(); onStart(e.clientX, e.clientY); });
            document.addEventListener('mousemove', e => onMove(e.clientX, e.clientY));
            document.addEventListener('mouseup', onEnd);
            this.btnEl.addEventListener('touchstart', e => {
                if (e.touches.length !== 1) return; e.preventDefault(); onStart(e.touches[0].clientX, e.touches[0].clientY);
            }, { passive: false });
            document.addEventListener('touchmove', e => {
                if (this._draggingBtn) { e.preventDefault(); onMove(e.touches[0].clientX, e.touches[0].clientY); }
            }, { passive: false });
            document.addEventListener('touchend', onEnd);
            this.btnEl.addEventListener('click', () => { if (!wasDragged) this.togglePanel(); });
        }

        /* ── 主面板(保持原有)──────────────────────────────────── */
        createPanel() {
            this.panelEl = document.createElement('div');
            this.panelEl.className = `${P}panel`;
            Object.assign(this.panelEl.style, { left: this.panelPos.x + 'px', top: this.panelPos.y + 'px' });
            this.panelEl.innerHTML = this._panelHTML();
            document.body.appendChild(this.panelEl);
            makeDraggable(this.panelEl, this.panelEl.querySelector(`.${P}ph`), {
                exclude: `.${P}hbtn`,
                onEnd: pos => { this.panelPos = pos; store.set(SK.PANEL_POS, pos); },
            });
            this._bindPanel();
            this.setStyle(this.style);
        }

        _panelHTML() {
            const presets = API_PRESETS.map(p =>
                `<span class="${P}preset" data-url="${esc(p.url)}" data-model="${esc(p.model)}">${esc(p.name)}</span>`
            ).join('');
            const tags = STYLES.map(s =>
                `<span class="${P}tag" data-style="${s.id}">${s.name}</span>`
            ).join('');
            return `
            <div class="${P}ph">
                <span class="${P}ph-title">&#10024;智语助手</span>
                <button class="${P}hbtn" id="${P}btn-cfg" title="设置">&#9881;</button>
                <button class="${P}hbtn" id="${P}btn-close" title="关闭 Esc">&#10005;</button>
            </div>
            <div class="${P}body">
              <div class="${P}settings" id="${P}settings">
                <span class="${P}set-label">快速切换服务商</span>
                <div class="${P}presets">${presets}</div>
                <span class="${P}set-label">API 配置</span>
                <div class="${P}set-row"><label>Base URL</label><input class="${P}set-inp" id="${P}s-url"   type="text"     spellcheck="false"></div>
                <div class="${P}set-row"><label>API Key</label> <input class="${P}set-inp" id="${P}s-key"   type="password"></div>
                <div class="${P}set-row"><label>Model</label>   <input class="${P}set-inp" id="${P}s-model" type="text"     spellcheck="false"></div>
                <span class="${P}set-label">系统提示词</span>
                <textarea class="${P}set-ta" id="${P}s-sys" rows="3"></textarea>
                <div class="${P}check-row">
                  <label><input type="checkbox" id="${P}s-bubble"> 选文弹窗</label>
                </div>
                <div class="${P}row">
                  <button class="${P}bp" id="${P}btn-save" style="font-size:12px;padding:7px 12px;">&#128190; 保存设置</button>
                  <button class="${P}bs" id="${P}btn-vis"  style="font-size:12px;padding:7px 12px;"></button>
                </div>
              </div>
              <span class="${P}lbl">操作模式</span>
              <div class="${P}tags" id="${P}tags">${tags}</div>
              <div class="${P}custom" id="${P}custom">
                <span style="font-size:11px;color:#64748b">自定义指令(直接写你的需求,无需加"请")</span>
                <textarea id="${P}custom-txt" placeholder="例:将文本改写为微信朋友圈风格,加适量emoji,不超过200字"></textarea>
              </div>
              <span class="${P}lbl">输入文本</span>
              <div class="${P}taw">
                <textarea class="${P}ta" id="${P}input" placeholder="在此输入或粘贴需要操作的文本(或选中文字按快捷键自动填入)…\n快捷键:\nCtrl+Shift+O 开启面板/自动填充\nCtrl+Shift+Enter 快捷运行\nCtrl+Shift+H 隐藏/显示悬浮球\nCtrl+Shift+D 开关选文弹窗"></textarea>
                <button class="${P}ta-x" id="${P}input-x" title="清空">&#10005;</button>
                <span class="${P}ta-cnt" id="${P}ic">0字</span>
              </div>
              <div class="${P}progress" id="${P}progress">
                <div class="${P}progress-label" id="${P}progress-label">处理中…</div>
                <div class="${P}progress-bar"><div class="${P}progress-fill" id="${P}progress-fill" style="width:0%"></div></div>
              </div>
              <div class="${P}row">
                <button class="${P}bp" id="${P}btn-opt">&#128640; 运行</button>
                <button class="${P}bs" id="${P}btn-stop" style="display:none;">&#9209; 停止</button>
              </div>
              <span class="${P}lbl">输出结果</span>
              <div class="${P}taw">
                <textarea class="${P}ta out" id="${P}output" placeholder="输出完成后结果将显示在此处…" readonly></textarea>
                <button class="${P}ta-x" id="${P}output-x" title="清空">&#10005;</button>
                <span class="${P}ta-cnt" id="${P}oc">0字</span>
              </div>
              <div class="${P}row">
                <button class="${P}bs" id="${P}btn-copy">&#128203; 复制</button>
                <button class="${P}bs" id="${P}btn-apply">&#9989; 应用</button>
              </div>
              <div class="${P}row">
                <button class="${P}bs" id="${P}btn-hist" style="flex:1;">&#128220; 历史记录</button>
              </div>
            </div>`;
        }

        _bindPanel() {
            const q  = id => this.panelEl.querySelector(`#${P}${id}`);
            const qa = sel => this.panelEl.querySelectorAll(sel);
            q('btn-close').addEventListener('click', () => this.closePanel());
            q('btn-cfg').addEventListener('click', () => {
                q('settings').classList.toggle('show');
                if (q('settings').classList.contains('show')) this._syncSettingsUI();
            });
            qa(`.${P}preset`).forEach(el => el.addEventListener('click', () => {
                q('s-url').value   = el.dataset.url;
                q('s-model').value = el.dataset.model;
            }));
            q('btn-save').addEventListener('click', () => this._saveSettingsUI());
            if (store.get(SK.BTN_VIS, null) === null) {
                store.set(SK.BTN_VIS, true);   // 首次安装默认显示
            }
            this.btnVis = store.get(SK.BTN_VIS, true) !== false;
            q('btn-vis').addEventListener('click', () => {
                this.btnVis = !this.btnVis;
                store.set(SK.BTN_VIS, this.btnVis);
                this.btnEl.style.display = this.btnVis ? 'flex' : 'none';
                q('btn-vis').textContent = this.btnVis ? '隐藏悬浮球' : '显示悬浮球';
                showToast(this.btnVis ? '悬浮球已显示' : '悬浮球已隐藏', 'info');
            });
            q('tags').addEventListener('click', e => {
                const t = e.target.closest(`.${P}tag`);
                if (t) this.setStyle(t.dataset.style);
            });
            const inpEl = q('input'), outEl = q('output');
            const autoResize = ta => { ta.style.height = 'auto'; ta.style.height = Math.min(ta.scrollHeight, 200) + 'px'; };
            inpEl.addEventListener('input', () => { q('ic').textContent = textStats(inpEl.value); autoResize(inpEl); });
            outEl.addEventListener('input', () => { q('oc').textContent = textStats(outEl.value); autoResize(outEl); });
            q('input-x').addEventListener('click',  () => { inpEl.value = ''; q('ic').textContent = '0字'; inpEl.style.height = ''; inpEl.focus(); });
            q('output-x').addEventListener('click', () => { outEl.value = ''; q('oc').textContent = '0字'; outEl.style.height = ''; });
            q('btn-opt').addEventListener('click',  () => this.optimize());
            q('btn-stop').addEventListener('click', () => this.stopOptimize());
            q('btn-copy').addEventListener('click', () => this.copyResult());
            q('btn-apply').addEventListener('click',() => this.applyResult());
            q('btn-hist').addEventListener('click', () => this.openHistory());
        }

        _syncSettingsUI() {
            const q = id => this.panelEl.querySelector(`#${P}${id}`);
            q('s-url').value        = this.cfg.apiBaseUrl;
            q('s-key').value        = this.cfg.apiKey;
            q('s-model').value      = this.cfg.model;
            q('s-sys').value        = this.cfg.systemPrompt;
            q('s-bubble').checked   = this.cfg.selBubble  !== false;
            q('btn-vis').textContent = this.btnVis ? '隐藏悬浮球' : '显示悬浮球';
        }

        _saveSettingsUI() {
            const q = id => this.panelEl.querySelector(`#${P}${id}`);
            this.cfg = {
                ...this.cfg,
                apiBaseUrl:   q('s-url').value.trim()   || DEFAULT_SETTINGS.apiBaseUrl,
                apiKey:       q('s-key').value.trim(),
                model:        q('s-model').value.trim() || DEFAULT_SETTINGS.model,
                systemPrompt: q('s-sys').value.trim()   || DEFAULT_SETTINGS.systemPrompt,
                selBubble:    q('s-bubble').checked,
            };
            saveSettings(this.cfg);
            showToast('&#9989; 设置已保存', 'success');
            this.panelEl.querySelector(`#${P}settings`).classList.remove('show');
        }

        setStyle(id) {
            this.style = id;
            this.panelEl.querySelectorAll(`.${P}tag`).forEach(t => t.classList.toggle('on', t.dataset.style === id));
            this.panelEl.querySelector(`#${P}custom`).classList.toggle('show', id === 'custom');
        }

        togglePanel() { this.open ? this.closePanel() : this.openPanel(); }
        openPanel(text = null) {
            if (this.open) {
                if (text !== null) this._setInput(text);
                return;
            }
            this.open = true;
            this.panelEl.classList.add('open');
            this._clampPanelToViewport();
            text !== null ? this._setInput(text) : setTimeout(() => this._grabText(), 80);
        }
        _clampPanelToViewport() {
            let x = parseInt(this.panelEl.style.left) || 0;
            let y = parseInt(this.panelEl.style.top)  || 0;
            x = Math.max(0, Math.min(window.innerWidth - Math.min(430, window.innerWidth - 12), x));
            y = Math.max(0, Math.min(window.innerHeight - 80, y));
            this.panelEl.style.left = x + 'px'; this.panelEl.style.top = y + 'px';
        }
        closePanel() {
            if (!this.open) return;
            this.open = false;
            this.panelEl.classList.remove('open');
            this.stopOptimize();
        }
        _setInput(text) {
            const ta = this.panelEl.querySelector(`#${P}input`);
            ta.value = text;
            ta.style.height = 'auto';
            ta.style.height = Math.min(ta.scrollHeight, 200) + 'px';
            this.panelEl.querySelector(`#${P}ic`).textContent = textStats(text);
        }
        _grabText() {
            const sel = window.getSelection()?.toString().trim();
            const ae  = document.activeElement;
            if (sel) {
                if (ae && ae.tagName === 'TEXTAREA') {
                    this.srcEl  = ae;
                    this.srcSel = { start: ae.selectionStart, end: ae.selectionEnd };
                } else if (ae && ae.tagName === 'INPUT' && isTextEl(ae)) {
                    this.srcEl  = ae;
                    this.srcSel = { start: ae.selectionStart, end: ae.selectionEnd };
                } else if (ae?.isContentEditable) {
                    this.srcEl = ae; this.srcSel = null;
                }
                this._setInput(sel);
                return;
            }
            if (!ae) return;
            if (ae.tagName === 'TEXTAREA' || (ae.tagName === 'INPUT' && isTextEl(ae))) {
                const val = ae.value?.trim();
                if (val) { this.srcEl = ae; this.srcSel = null; this._setInput(val); }
            } else if (ae.isContentEditable && ae.innerText.trim()) {
                this.srcEl = ae; this.srcSel = null; this._setInput(ae.innerText.trim());
            }
        }

        /* ── 核心优化流程(完整保留)──────────────────────────────── */
        async optimize() {
            if (this.loading) return;
            const inpEl = this.panelEl.querySelector(`#${P}input`);
            const outEl = this.panelEl.querySelector(`#${P}output`);
            const text  = inpEl.value.trim();
            if (!text) { showToast('请输入文本', 'warning'); inpEl.focus(); return; }
            if (!this.cfg.apiKey) { showToast('请先在设置中填写 API Key', 'warning'); this.panelEl.querySelector(`#${P}settings`).classList.add('show'); this._syncSettingsUI(); return; }
            let prompt = STYLES.find(s => s.id === this.style)?.prompt ?? '';
            if (this.style === 'custom') {
                const raw = this.panelEl.querySelector(`#${P}custom-txt`).value.trim();
                prompt = raw ? `【自定义模式】\n${raw}\n\n只输出处理后的文本,不加任何解释或前缀。` : '请优化以下文本,只返回结果。';
            }
            this._setLoading(true);
            outEl.value = ''; outEl.className = `${P}ta out`;
            this.panelEl.querySelector(`#${P}oc`).textContent = '0字';
            try {
                const chunks = splitIntoChunks(text, MAX_CHARS_PER_CHUNK);
                if (chunks.length > 1) await this._optimizeLong(chunks, prompt, outEl);
                else if (this.cfg.streaming) await this._optimizeStream(text, prompt, outEl);
                else { const result = await this._callWithRetry(text, prompt); outEl.value = result; this._syncOutputUI(outEl); }
                const finalResult = outEl.value.trim();
                if (finalResult) {
                    this.history = [{ original: text, optimized: finalResult, style: this.style, ts: Date.now() }, ...this.history].slice(0, MAX_HIST);
                    saveHistory(this.history);
                    showToast('&#9989; 运行完成', 'success');
                }
            } catch (err) { const msg = err?.message ?? String(err); if (/abort/i.test(msg)) { outEl.className = `${P}ta out`; showToast('&#9209; 已停止', 'info'); } else { outEl.className = `${P}ta out err`; showToast(this._errMsg(err), 'error', 4000); } }
            finally { this._setLoading(false); this._hideProgress(); this.abort = null; }
        }
        async _optimizeStream(text, prompt, outEl) { outEl.className = `${P}ta stream`; let acc=''; await this._callStream(text, prompt, ch=>{ acc+=ch; outEl.value=acc; this._syncOutputUI(outEl); }); outEl.className=`${P}ta out`; return acc; }
        async _optimizeLong(chunks, prompt, outEl) { const total=chunks.length; this._showProgress(0,total); showToast(`&#128196; 长文本已分为 ${total} 块处理`, 'info',3000); const results=[]; for(let i=0;i<total;i++){ if(!this.loading)break; this._showProgress(i,total); let partResult; if(this.cfg.streaming){ let acc=''; outEl.className=`${P}ta stream`; await this._callStream(chunks[i], prompt, ch=>{ acc+=ch; outEl.value=results.join('\n\n')+(results.length?'\n\n':'')+acc+'▌'; this._syncOutputUI(outEl); }); partResult=acc; } else partResult=await this._callWithRetry(chunks[i], prompt); results.push(partResult); this._showProgress(i+1,total); } const combined=results.join('\n\n'); outEl.value=combined; outEl.className=`${P}ta out`; this._syncOutputUI(outEl); return combined; }
        _syncOutputUI(outEl) { outEl.style.height='auto'; outEl.style.height=Math.min(outEl.scrollHeight,200)+'px'; outEl.scrollTop=outEl.scrollHeight; this.panelEl.querySelector(`#${P}oc`).textContent=textStats(outEl.value.replace(/▌$/,'')); }
        _showProgress(done,total){ const el=this.panelEl.querySelector(`#${P}progress`); const label=this.panelEl.querySelector(`#${P}progress-label`); const fill=this.panelEl.querySelector(`#${P}progress-fill`); el.classList.add('show'); label.textContent=`处理分块 ${done} / ${total}`; fill.style.width=`${total?(done/total)*100:0}%`; }
        _hideProgress(){ this.panelEl.querySelector(`#${P}progress`).classList.remove('show'); }
        stopOptimize(){ if(typeof this.abort ==='function'){ this.abort(); this.abort=null; } }
        _setLoading(on){ this.loading=on; const opt=this.panelEl.querySelector(`#${P}btn-opt`); const stop=this.panelEl.querySelector(`#${P}btn-stop`); if(on){ opt.disabled=true; opt.innerHTML=`<span class="${P}spin"></span> 运行中…`; stop.style.display='flex'; }else{ opt.disabled=false; opt.innerHTML='&#128640; 运行'; stop.style.display='none'; } }
        _errMsg(err){ const m=err instanceof Error?(err.message??''):String(err??'未知错误'); if(/401|403/.test(m)) return '&#10060; API Key 无效或无权限'; if(/429/.test(m)) return '&#10060; 请求过于频繁,请稍候再试'; if(/402|quota|insufficient/.test(m)) return '&#10060; API 额度不足'; if(/timeout/.test(m)) return '&#10060; 请求超时,请检查网络'; if(/NetworkError|network|fetch/i.test(m)) return '&#10060; 网络连接失败'; if(/empty|空/.test(m)) return '&#10060; 模型返回内容为空,请重试'; return `&#10060; ${m.slice(0,80)||'未知错误'}`; }
        _getTemperature(){ const styleCfg=STYLES.find(s=>s.id===this.style); if(this.style==='custom'||styleCfg?.temperature===null) return this.cfg.temperature; return styleCfg?.temperature??this.cfg.temperature; }
        _buildMessages(text,stylePrompt){ const systemContent=stylePrompt?`${this.cfg.systemPrompt}\n\n---\n${stylePrompt}`:this.cfg.systemPrompt; return [{role:'system',content:systemContent},{role:'user',content:`<input>\n${text}\n</input>`}]; }
        _buildBody(text,prompt,stream){ return JSON.stringify({model:this.cfg.model,messages:this._buildMessages(text,prompt),temperature:this._getTemperature(),max_tokens:this.cfg.maxTokens,stream}); }
        _apiURL(){ return this.cfg.apiBaseUrl.replace(/\/+$/,'')+'/chat/completions'; }
        async _callWithRetry(text,prompt){ const maxRetries=this.cfg.autoRetry?(this.cfg.maxRetries||3):1; let lastErr; for(let attempt=0;attempt<maxRetries;attempt++){ try{ return await this._callAPI(text,prompt); }catch(err){ lastErr=err; const msg=err?.message??''; if(/abort|401|403|402|quota|insufficient/i.test(msg)) throw err; if(attempt<maxRetries-1){ const delay=1000*Math.pow(2,attempt); showToast(`&#9888;&#65039; 请求失败,${delay/1000}s 后重试 (${attempt+1}/${maxRetries-1})…`,'warning',delay); await new Promise(r=>setTimeout(r,delay)); } } } throw lastErr; }
        _callAPI(text,prompt){ return new Promise((resolve,reject)=>{ let settled=false; const settle=(fn,val)=>{ if(settled)return; settled=true; fn(val); }; let xhrHandle=null; this.abort=()=>{ xhrHandle?.abort?.(); settle(reject,new Error('abort')); }; xhrHandle=GM_xmlhttpRequest({method:'POST',url:this._apiURL(),headers:{'Content-Type':'application/json',Authorization:`Bearer ${this.cfg.apiKey}`},data:this._buildBody(text,prompt,false),timeout:60000,onload:r=>{ if(r.status===200){ try{ const d=JSON.parse(r.responseText); const c=d.choices?.[0]?.message?.content; c?settle(resolve,c.trim()):settle(reject,new Error('响应内容为空')); }catch{ settle(reject,new Error('解析响应失败')); } }else{ let msg=`HTTP ${r.status}`; try{ const e=JSON.parse(r.responseText); msg+=': '+(e.error?.message??e.message??''); }catch{} settle(reject,new Error(msg)); } },onerror:()=>settle(reject,new Error('NetworkError')),ontimeout:()=>settle(reject,new Error('timeout')),onabort:()=>settle(reject,new Error('abort'))}); }); }
        _callStream(text,prompt,onChunk){ return new Promise((resolve,reject)=>{ let done=false,lineBuffer='',processed=0; let xhrHandle=null; this.abort=()=>{ if(done)return; done=true; xhrHandle?.abort?.(); reject(new Error('abort')); }; const parseLine=line=>{ const t=line.trim(); if(!t||t==='data: [DONE]')return; if(t.startsWith('data: ')){ try{ const d=JSON.parse(t.slice(6)); const delta=d.choices?.[0]?.delta?.content; if(delta) onChunk(delta); }catch{} } }; const flushBuffer=buf=>{ const lines=buf.split('\n'); const remaining=lines.pop()??''; lines.forEach(parseLine); return remaining; }; xhrHandle=GM_xmlhttpRequest({method:'POST',url:this._apiURL(),headers:{'Content-Type':'application/json',Authorization:`Bearer ${this.cfg.apiKey}`},data:this._buildBody(text,prompt,true),timeout:120000,onprogress:r=>{ if(done)return; const newData=r.responseText.slice(processed); processed=r.responseText.length; lineBuffer+=newData; lineBuffer=flushBuffer(lineBuffer); },onload:r=>{ if(done)return; done=true; if(lineBuffer.trim()) parseLine(lineBuffer); lineBuffer=''; if(r.status>=400){ let msg=`HTTP ${r.status}`; try{ const e=JSON.parse(r.responseText); msg+=': '+(e.error?.message??''); }catch{} reject(new Error(msg)); }else{ resolve(); } },onerror:()=>{ if(!done){ done=true; reject(new Error('NetworkError')); } },ontimeout:()=>{ if(!done){ done=true; reject(new Error('timeout')); } },onabort:()=>{ if(!done){ done=true; reject(new Error('abort')); } } }); }); }
        copyResult(){ const t=this.panelEl.querySelector(`#${P}output`).value.trim(); if(!t){ showToast('暂无结果可复制','warning'); return; } try{ GM_setClipboard(t); showToast('&#9989; 已复制','success'); } catch{ navigator.clipboard?.writeText(t).then(()=>showToast('&#9989; 已复制','success')).catch(()=>showToast('复制失败','error')); } }
        applyResult(){ const t=this.panelEl.querySelector(`#${P}output`).value.trim(); if(!t){ showToast('暂无结果可应用','warning'); return; } this._applyToInput(t); }
        _applyToInput(text){ const setNative=(el,val)=>{ try{ const desc=Object.getOwnPropertyDescriptor(Object.getPrototypeOf(el),'value'); if(desc?.set) desc.set.call(el,val); else el.value=val; }catch{ el.value=val; } el.dispatchEvent(new Event('input',{bubbles:true})); el.dispatchEvent(new Event('change',{bubbles:true})); }; const applyToEditable=el=>{ el.focus(); const sel=window.getSelection(); const range=document.createRange(); range.selectNodeContents(el); sel.removeAllRanges(); sel.addRange(range); if(!document.execCommand('insertText',false,text)){ el.innerText=text; el.dispatchEvent(new Event('input',{bubbles:true})); } }; const applyEl=el=>{ if(el.isContentEditable){ applyToEditable(el); } else{ setNative(el,text); el.focus(); } }; if(this.srcEl&&document.contains(this.srcEl)&&isTextEl(this.srcEl)){ try{ if(this.srcSel&&!this.srcEl.isContentEditable&&this.srcEl.selectionStart!==undefined){ const{start,end}=this.srcSel; setNative(this.srcEl,this.srcEl.value.slice(0,start)+text+this.srcEl.value.slice(end)); this.srcEl.selectionStart=start; this.srcEl.selectionEnd=start+text.length; this.srcEl.focus(); }else{ applyEl(this.srcEl); } showToast('&#9989; 已应用到输入框','success'); return; }catch{} } const ae=document.activeElement; if(ae&&!this.panelEl.contains(ae)&&isTextEl(ae)){ try{ applyEl(ae); showToast('&#9989; 已应用','success'); return; }catch{} } const fallback=()=>showToast('&#128203; 已复制到剪贴板(未检测到目标输入框,请手动粘贴)','info'); try{ GM_setClipboard(text); fallback(); }catch{ navigator.clipboard?.writeText(text).then(fallback).catch(()=>showToast('请手动粘贴','warning')); } }

        /* ─── 统一的划词气泡(新增快捷键开关)────────────────────── */
        createBubble() {
            this.bubbleEl = document.createElement('div');
            this.bubbleEl.className = `${P}bubble`;
            this.bubbleEl.innerHTML = `
                <button class="${P}bbtn" id="${P}bb-enhance">&#128640; 增强</button>
                <div class="${P}bsep"></div>
                <button class="${P}bbtn" id="${P}bb-quick-en">&#128221; 译英</button>
                <div class="${P}bsep"></div>
                <button class="${P}bbtn" id="${P}bb-quick-zh">&#128221; 译中</button>
            `;
            document.body.appendChild(this.bubbleEl);

            // 隐藏气泡并更新可见标志
            const hide = () => {
                this.bubbleEl.style.display = 'none';
                this._bubbleVisible = false;
            };
            // 显示气泡(由内部调用时同时记录位置和文本)
            const show = (x, y) => {
                this.bubbleEl.style.display = 'flex';
                const bw = this.bubbleEl.offsetWidth  || 220;
                const bh = this.bubbleEl.offsetHeight || 34;
                let l = x - bw / 2;
                let t = y - bh - 10;
                l = Math.max(8, Math.min(window.innerWidth - bw - 8, l));
                if (t < 8) t = y + 12;
                this.bubbleEl.style.left = l + 'px';
                this.bubbleEl.style.top  = t + 'px';
                this._bubbleVisible = true;
            };

            // 公共显示逻辑:从当前选中文本生成气泡
            const tryShowFromSelection = () => {
                if (!this.cfg.selBubble) return false;
                if (this.open) return false;
                let selText = window.getSelection()?.toString().trim() ?? '';
                let anchorX = 0, anchorY = 0, hasAnchor = false;
                let selRange = null;
                if (selText.length >= 1) {
                    try {
                        const selection = window.getSelection();
                        if (selection.rangeCount > 0) {
                            const r = selection.getRangeAt(0);
                            selRange = r.cloneRange();
                            const rect = r.getBoundingClientRect();
                            if (rect.width > 0 || rect.height > 0) {
                                anchorX = rect.left + rect.width / 2;
                                anchorY = rect.top;
                                hasAnchor = true;
                            }
                        }
                    } catch {}
                }
                if (!selText || !hasAnchor) {
                    const ae = document.activeElement;
                    if (ae && isTextEl(ae) && !ae.isContentEditable) {
                        const { selectionStart: s, selectionEnd: end, value } = ae;
                        if (typeof s === 'number' && s !== end) {
                            const candidate = value.slice(s, end).trim();
                            if (candidate.length >= 3) {
                                selText = candidate;
                                const r = ae.getBoundingClientRect();
                                anchorX  = r.left + r.width / 2;
                                anchorY  = r.top;
                                hasAnchor = true;
                                selRange = null;
                            }
                        }
                    }
                }
                if (selText && hasAnchor) {
                    this._bubbleText = selText;
                    this.lastSelectionRange = selRange;
                    show(anchorX, anchorY);
                    return true;
                }
                return false;
            };

            // 鼠标释放时尝试显示气泡
            document.addEventListener('mouseup', e => {
                if (this.bubbleEl.contains(e.target)) return;
                setTimeout(() => {
                    if (tryShowFromSelection()) return;
                    hide();
                }, 10);
            });

            document.addEventListener('mousedown', e => { if (!this.bubbleEl.contains(e.target)) hide(); });
            document.addEventListener('keydown',   e => { if (e.key === 'Escape') hide(); });

            // 增强模式:打开主面板并运行增强
            this.bubbleEl.querySelector(`#${P}bb-enhance`).addEventListener('click', () => {
                hide();
                this.setStyle('enhance');
                this.openPanel(this._bubbleText);
                setTimeout(() => this.optimize(), 300);
            });

            // 统一翻译入口:快速翻译浮层
            this.bubbleEl.querySelector(`#${P}bb-quick-en`).addEventListener('click', (e) => {
                e.stopPropagation();
                hide();
                this._quickTranslate(this._bubbleText, 'en', e);
            });
            this.bubbleEl.querySelector(`#${P}bb-quick-zh`).addEventListener('click', (e) => {
                e.stopPropagation();
                hide();
                this._quickTranslate(this._bubbleText, 'zh', e);
            });

            // 为快捷键暴露公共方法
            this._showBubbleFromSelection = () => tryShowFromSelection();
            this._hideBubble = () => hide();
        }

        /* ─── 快速翻译浮层(带复制/替换/面板编辑)──────────────── */
        createQuickTranslatePanel() {
            this.quickPanel = document.createElement('div');
            this.quickPanel.className = `${P}quick-panel`;
            this.quickPanel.innerHTML = `
                <div class="${P}quick-head">
                    <span>&#9889; 划词翻译</span>
                    <button class="${P}quick-close">&#10005;</button>
                </div>
                <div class="${P}quick-body"></div>
                <div class="${P}quick-actions">
                    <button class="${P}quick-copy">&#128203; 复制译文</button>
                    <button class="${P}quick-replace">&#128260; 替换原文</button>
                    <button class="${P}quick-paneledit">&#9999;&#65039; 面板编辑</button>
                </div>
            `;
            document.body.appendChild(this.quickPanel);
            const head = this.quickPanel.querySelector(`.${P}quick-head`);
            makeDraggable(this.quickPanel, head, { exclude: 'button' });
            this.quickPanel.querySelector(`.${P}quick-close`).addEventListener('click', () => this._hideQuickPanel());
            this.quickPanel.querySelector(`.${P}quick-copy`).addEventListener('click', () => {
                const body = this.quickPanel.querySelector(`.${P}quick-body`);
                const text = body.innerText;
                if (text && !text.includes('翻译中') && !text.includes('失败')) {
                    GM_setClipboard(text);
                    showToast('译文已复制', 'success');
                } else showToast('暂无译文可复制', 'warning');
            });
            this.quickPanel.querySelector(`.${P}quick-replace`).addEventListener('click', () => {
                const body = this.quickPanel.querySelector(`.${P}quick-body`);
                const translation = body.innerText;
                if (!translation || translation.includes('翻译中') || translation.includes('失败')) {
                    showToast('没有可用的译文', 'warning');
                    return;
                }
                this._replaceSelectedText(translation);
                this._hideQuickPanel();
                showToast('已替换原文', 'success');
            });
            this.quickPanel.querySelector(`.${P}quick-paneledit`).addEventListener('click', () => {
                if (!this._lastQuickOriginal) {
                    showToast('无可编辑的原文', 'warning');
                    return;
                }
                const styleId = this._lastQuickTargetLang === 'en' ? 'trans_en' : 'trans_zh';
                this.setStyle(styleId);
                this.openPanel(this._lastQuickOriginal);
                this._hideQuickPanel();
                showToast('已打开面板,可继续编辑或运行', 'success');
            });
        }

        _hideQuickPanel() {
            if (this.quickPanel) this.quickPanel.classList.remove('show');
            if (this.activeQuickAbort) { this.activeQuickAbort(); this.activeQuickAbort = null; }
        }

        async _quickTranslate(text, targetLang, event) {
            if (!text || text.length < 1) return;
            if (!this.cfg.apiKey) {
                showToast('请先在设置中填写 API Key', 'warning');
                return;
            }
            if (this.activeQuickAbort) { this.activeQuickAbort(); this.activeQuickAbort = null; }
            const bodyEl = this.quickPanel.querySelector(`.${P}quick-body`);
            bodyEl.innerHTML = `<div class="${P}quick-loading"><span class="${P}spin"></span> 翻译中…</div>`;
            this.quickPanel.classList.add('show');
            let x = 0, y = 0;
            if (event && event.clientX) { x = event.clientX; y = event.clientY; }
            else if (this.lastSelectionRange) {
                const rect = this.lastSelectionRange.getBoundingClientRect();
                x = rect.left + rect.width/2; y = rect.top;
            } else { x = window.innerWidth/2; y = window.innerHeight/2; }
            const w = this.quickPanel.offsetWidth || 340;
            const h = this.quickPanel.offsetHeight || 200;
            let left = Math.min(window.innerWidth - w - 10, Math.max(10, x - w/2));
            let top = Math.min(window.innerHeight - h - 10, Math.max(40, y - h - 15));
            if (top < 40) top = y + 20;
            this.quickPanel.style.left = left + 'px';
            this.quickPanel.style.top = top + 'px';

            const styleId = targetLang === 'en' ? 'trans_en' : 'trans_zh';
            const styleCfg = STYLES.find(s => s.id === styleId);
            if (!styleCfg) { bodyEl.innerHTML = '<div class="quick-err">翻译配置错误</div>'; return; }
            const prompt = styleCfg.prompt;
            try {
                const result = await this._callQuickAPI(text, prompt);
                bodyEl.innerHTML = `<div style="white-space:pre-wrap;">${esc(result)}</div>`;
                this._lastQuickOriginal = text;
                this._lastQuickTargetLang = targetLang;
                this.history = [
                    { original: text, optimized: result, style: styleId, ts: Date.now() },
                    ...this.history
                ].slice(0, MAX_HIST);
                saveHistory(this.history);
            } catch (err) {
                const errMsg = this._errMsg(err);
                bodyEl.innerHTML = `<div class="${P}quick-err">${errMsg}</div>`;
                showToast(errMsg, 'error', 3000);
            } finally {
                this.activeQuickAbort = null;
            }
        }

        _callQuickAPI(text, prompt) {
            return new Promise((resolve, reject) => {
                let settled = false;
                const settle = (fn, val) => { if (settled) return; settled = true; fn(val); };
                let xhr = null;
                this.activeQuickAbort = () => { xhr?.abort?.(); settle(reject, new Error('abort')); };
                const messages = this._buildMessages(text, prompt);
                const body = JSON.stringify({
                    model: this.cfg.model,
                    messages,
                    temperature: 0.3,
                    max_tokens: Math.min(1500, this.cfg.maxTokens),
                    stream: false
                });
                xhr = GM_xmlhttpRequest({
                    method: 'POST',
                    url: this._apiURL(),
                    headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.cfg.apiKey}` },
                    data: body,
                    timeout: 45000,
                    onload: r => {
                        if (r.status === 200) {
                            try {
                                const d = JSON.parse(r.responseText);
                                const c = d.choices?.[0]?.message?.content;
                                c ? settle(resolve, c.trim()) : settle(reject, new Error('翻译结果为空'));
                            } catch { settle(reject, new Error('解析失败')); }
                        } else {
                            let msg = `HTTP ${r.status}`;
                            try { const e = JSON.parse(r.responseText); msg += ': ' + (e.error?.message ?? e.message ?? ''); } catch { }
                            settle(reject, new Error(msg));
                        }
                    },
                    onerror: () => settle(reject, new Error('NetworkError')),
                    ontimeout: () => settle(reject, new Error('timeout')),
                    onabort: () => settle(reject, new Error('abort'))
                });
            });
        }

        _replaceSelectedText(newText) {
            if (!newText) return;
            if (this.lastSelectionRange && this.lastSelectionRange.startContainer) {
                try {
                    const range = this.lastSelectionRange;
                    range.deleteContents();
                    const textNode = document.createTextNode(newText);
                    range.insertNode(textNode);
                    range.setStartAfter(textNode);
                    range.collapse(true);
                    window.getSelection()?.removeAllRanges();
                    window.getSelection()?.addRange(range);
                    return;
                } catch (e) { console.warn('替换Range失败', e); }
            }
            const ae = document.activeElement;
            if (ae && isTextEl(ae)) {
                if (!ae.isContentEditable && ae.selectionStart !== undefined) {
                    const start = ae.selectionStart, end = ae.selectionEnd;
                    const val = ae.value;
                    ae.value = val.slice(0, start) + newText + val.slice(end);
                    ae.selectionStart = start;
                    ae.selectionEnd = start + newText.length;
                    ae.dispatchEvent(new Event('input', { bubbles: true }));
                } else if (ae.isContentEditable) {
                    const sel = window.getSelection();
                    if (sel.rangeCount) {
                        const range = sel.getRangeAt(0);
                        range.deleteContents();
                        range.insertNode(document.createTextNode(newText));
                        range.collapse(false);
                    } else {
                        ae.innerText = newText;
                        ae.dispatchEvent(new Event('input', { bubbles: true }));
                    }
                }
            } else {
                showToast('未检测到可替换的文本区域,已复制到剪贴板', 'info');
                GM_setClipboard(newText);
            }
        }

        /* ── 历史记录 Modal (完整保留) ──────────────────────────── */
        createHistoryModal() {
            this.histEl = document.createElement('div');
            this.histEl.className = `${P}hov`;
            this.histEl.innerHTML = `
                <div class="${P}hbox" id="${P}hbox">
                    <div class="${P}hhdr" id="${P}hhdr">
                        <h3 class="${P}htitle">&#128220; 运行历史</h3>
                        <input class="${P}hsearch" id="${P}hsearch" placeholder="搜索历史记录…">
                        <button style="background:none;border:none;font-size:20px;cursor:pointer;color:#64748b;padding:0 4px;flex-shrink:0;" id="${P}hclose">&#10005;</button>
                    </div>
                    <div class="${P}hlist" id="${P}hlist"></div>
                    <div class="${P}hfoot">
                        <button class="${P}bs danger" id="${P}hclear">&#128465; 清空历史</button>
                        <button class="${P}bs"         id="${P}hexport">&#11015; 导出 JSON</button>
                    </div>
                </div>`;
            document.body.appendChild(this.histEl);
            const box = this.histEl.querySelector(`#${P}hbox`);
            const hdr = this.histEl.querySelector(`#${P}hhdr`);
            makeDraggable(box, hdr, { exclude: `input,button`, onEnd: pos => store.set(SK.HIST_POS, pos) });
            const savedPos = store.get(SK.HIST_POS, null);
            if (savedPos) { box.style.transform = 'none'; box.style.left = savedPos.x + 'px'; box.style.top  = savedPos.y + 'px'; }
            this.histEl.querySelector(`#${P}hclose`).addEventListener('click', () => this.closeHistory());
            this.histEl.addEventListener('click', e => { if (e.target === this.histEl) this.closeHistory(); });
            this.histEl.querySelector(`#${P}hsearch`).addEventListener('input', e => this._renderHistory(e.target.value));
            this.histEl.querySelector(`#${P}hclear`).addEventListener('click', () => { if (!confirm('确认清空所有历史记录?')) return; this.history = []; saveHistory([]); this._renderHistory(); showToast('历史记录已清空', 'info'); });
            this.histEl.querySelector(`#${P}hexport`).addEventListener('click', () => this._exportHistory());
            this.histEl.querySelector(`#${P}hlist`).addEventListener('click', e => { const btn = e.target.closest('button[data-a]'); if (!btn) return; const idx = parseInt(btn.dataset.i); if (isNaN(idx) || !this.history[idx]) return; const h = this.history[idx]; switch (btn.dataset.a) { case 'copy': try{ GM_setClipboard(h.optimized); } catch{ navigator.clipboard?.writeText(h.optimized); } showToast('已复制', 'success'); break; case 'apply': this._applyToInput(h.optimized); this.closeHistory(); break; case 'del': this.history.splice(idx,1); saveHistory(this.history); this._renderHistory(this.histEl.querySelector(`#${P}hsearch`).value); break; } });
        }
        openHistory()  { this._renderHistory(); this.histEl.classList.add('open'); }
        closeHistory() { this.histEl.classList.remove('open'); }
        _renderHistory(filter = '') { const list=this.histEl.querySelector(`#${P}hlist`); const filtered=filter?this.history.filter(h=>h.original.includes(filter)||h.optimized.includes(filter)):this.history; if(!filtered.length){ list.innerHTML=`<div class="${P}hempty">${filter?'&#128269; 无匹配记录':'暂无历史记录'}</div>`; return; } list.innerHTML=filtered.map(h=>{ const idx=this.history.indexOf(h); const style=STYLES.find(s=>s.id===h.style)?.name??'自定义'; const time=new Date(h.ts).toLocaleString('zh-CN',{month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'}); const oPrev=esc(h.original.slice(0,120)+(h.original.length>120?'…':'')); const rPrev=esc(h.optimized.slice(0,120)+(h.optimized.length>120?'…':'')); return `<div class="${P}hi"><div class="${P}hi-meta"><span>${time}</span><span class="${P}hi-badge">${esc(style)}</span></div><div class="${P}hi-cols"><div class="${P}hi-col"><strong style="font-size:10px;color:#64748b;display:block;margin-bottom:2px">原文</strong>${oPrev}</div><div class="${P}hi-col"><strong style="font-size:10px;color:#166534;display:block;margin-bottom:2px">结果</strong>${rPrev}</div></div><div class="${P}hi-acts"><button data-a="copy" data-i="${idx}">&#128203; 复制</button><button data-a="apply" data-i="${idx}">&#9989; 应用</button><button data-a="del" data-i="${idx}" class="del">&#128465;</button></div></div>`; }).join(''); }
        _exportHistory(){ if(!this.history.length){ showToast('暂无历史记录可导出','warning'); return; } try{ const blob=new Blob([JSON.stringify(this.history,null,2)],{type:'application/json'}); const url=URL.createObjectURL(blob); const a=document.createElement('a'); a.href=url; a.download=`tmo-history-${Date.now()}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); showToast('&#9989; 历史已导出','success'); }catch(e){ showToast('导出失败:'+(e?.message??e),'error'); } }

        /* ── 全局快捷键(新增 Ctrl+Shift+D 开关选文弹窗)────────── */
        bindKeys() {
            document.addEventListener('keydown', e => {
                // Ctrl+Shift+O 打开面板
                if (e.ctrlKey && e.shiftKey && e.code === 'KeyO') {
                    e.preventDefault();
                    this.togglePanel();
                    return;
                }
                // Ctrl+Shift+H 隐藏/显示悬浮球
                if (e.ctrlKey && e.shiftKey && e.code === 'KeyH') {
                    e.preventDefault();
                    this.btnVis = !this.btnVis;
                    store.set(SK.BTN_VIS, this.btnVis);
                    this.btnEl.style.display = this.btnVis ? 'flex' : 'none';
                    showToast(this.btnVis ? '悬浮球已显示' : '悬浮球已隐藏', 'info');
                    return;
                }
                // Ctrl+Shift+D 开关选文弹窗
                if (e.ctrlKey && e.shiftKey && e.code === 'KeyD') {
                    e.preventDefault();

                    // 切换全局开关
                    this.cfg.selBubble = !this.cfg.selBubble;
                    saveSettings(this.cfg);

                    // 同步设置面板中的复选框状态(如果面板存在)
                    if (this.panelEl) {
                        const cb = this.panelEl.querySelector(`#${P}s-bubble`);
                        if (cb) cb.checked = this.cfg.selBubble;
                    }

                    if (this.cfg.selBubble) {
                        // 开启选文弹窗:尝试立即显示当前选中文本的气泡
                        const shown = this._showBubbleFromSelection?.();
                        showToast(shown ? '选文弹窗已开启' : '选文弹窗已开启(未检测到选中文本)', 'info');
                    } else {
                        // 关闭选文弹窗:隐藏当前可见的气泡,并禁用后续弹出
                        this._hideBubble?.();
                        showToast('选文弹窗已关闭', 'info');
                    }
                    return;
                }
                // Ctrl+Shift+Enter 在主面板运行时触发
                if (e.ctrlKey && e.shiftKey && e.code === 'Enter' && this.open) {
                    e.preventDefault();
                    this.optimize();
                    return;
                }
                // ESC: 关闭快速翻译浮层,再关闭主面板/历史(如果没有其他弹窗)
                if (e.key === 'Escape') {
                    // 优先关闭快速翻译浮层
                    if (this.quickPanel && this.quickPanel.classList.contains('show')) {
                        e.preventDefault();
                        this._hideQuickPanel();
                        return;
                    }
                    // 关闭选文弹窗
                    if (this._bubbleVisible && this.bubbleEl && this.bubbleEl.style.display === 'flex') {
                        e.preventDefault();
                        this._hideBubble();
                        return;
                    }
                    // 再关闭历史模态框
                    if (this.histEl && this.histEl.classList.contains('open')) {
                        e.preventDefault();
                        this.closeHistory();
                        return;
                    }
                    // 最后关闭主面板
                    if (this.open) {
                        e.preventDefault();
                        this.closePanel();
                        return;
                    }
                }
            });
            window.addEventListener('resize', () => {
                const mx = window.innerWidth - 50, my = window.innerHeight - 50;
                if (this.btnPos.x > mx) { this.btnPos.x = mx; this.btnEl.style.left = mx + 'px'; }
                if (this.btnPos.y > my) { this.btnPos.y = my; this.btnEl.style.top  = my + 'px'; }
            });
        }
    }

    /* ── 启动 ──────────────────────────────────────────────────── */
    const boot = () => setTimeout(() => new TextOptimizer(), 500);
    document.readyState === 'loading'
        ? document.addEventListener('DOMContentLoaded', boot)
        : boot();})();

智语助手.zip

19.81 KB, 下载次数: 116, 下载积分: 吾爱币 -1 CB

脚本下载

免费评分

参与人数 6威望 +1 吾爱币 +14 热心值 +6 收起 理由
xiangjb1 + 1 + 1 鼓励转贴优秀软件安全工具和文档!
alifeng + 1 + 1 用心讨论,共获提升!
top777 + 1 + 1 谢谢@Thanks!
苏紫方璇 + 1 + 10 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
dadas + 1 热心回复!
fakerx + 1 + 1 谢谢@Thanks!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

 楼主| PJlay 发表于 2026-5-21 22:36
本帖最后由 PJlay 于 2026-5-21 22:38 编辑

图标显示的问题,可能是网站代码上传不兼容或者解析行为不一致的问题,我改了几次都不行,大家只能自行改一下了。

不二 发表于 2026-5-21 21:53
本帖最后由 不二 于 2026-5-21 21:58 编辑

占楼,
Q1:&#10024;   直接写字符:&#10024;(浏览器支持,不知道为什么显示&#10024; )

Q2:&#10060;直接写字符:&#10060;,不知道为什么显示的十进制
 楼主| PJlay 发表于 2026-5-21 22:08
不二 发表于 2026-5-21 21:53
占楼,
Q1:&#10024;   直接写字符:&#10024;(浏览器支持,不知道为什么显示&#10024; )
Q2:&#10060;直 ...

这个应该是浏览器解析行为不一致导致的,然后直接显示为原始实体字符串而非对应字符了,这个可以手动更改输入 对应符号就行,显示问题
江樾小王子 发表于 2026-5-21 21:09
添加了刷新没有看到小球球
shengforever 发表于 2026-5-21 21:12
看说明挺不错的,试试看
SQWBL 发表于 2026-5-21 21:42
安装后没看见有小球
 楼主| PJlay 发表于 2026-5-21 21:45
SQWBL 发表于 2026-5-21 21:42
安装后没看见有小球

试试快捷键能打开吗
 楼主| PJlay 发表于 2026-5-21 21:46
江樾小王子 发表于 2026-5-21 21:09
添加了刷新没有看到小球球

试试快捷键能打开吗
白棋夏 发表于 2026-5-21 21:59
没有看见啊
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-8-29 05:52

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表