vue + onlyoffice 自定义插件的实现(OnlyOffice 插件:AI 智能编辑)。

OnlyOffice 插件:AI 智能编辑

目录结构

onlyoffice-plugin-ai/

├── config.json # 插件元数据、GUID、尺寸、事件

├── index.html # 面板 UI,引用 OnlyOffice 提供的 ../v1/plugins*.js

├── scripts/

│ └── code.js # Asc.Api 调用、流式请求、Replace/Paste 等逻辑

├── resources/light/ # icon.png、icon@2x.png(可用 create_icons.py 生成)

├── translations/ # 多语言 JSON

├── create_icons.py # 生成本文件夹内图标

└── README.md # 本说明

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script type="text/javascript" src="../v1/plugins.js"></script>
    <script type="text/javascript" src="../v1/plugins-ui.js"></script>
    <link rel="stylesheet" href="../v1/plugins.css">
    <style>
        * { box-sizing:border-box; margin:0; padding:0; }
        body { font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
            font-size:14px; color:#333; padding:0; background:#fff;
            display:flex; flex-direction:column; height:100vh; overflow:hidden; }

        /* 标签页 */
        .tabs { display:flex; border-bottom:1px solid #eee; background:#fafafa; flex-shrink:0; }
        .tab { flex:1; padding:10px 0; text-align:center; font-size:13px; cursor:pointer;
            color:#999; border-bottom:2px solid transparent; transition:all .15s; }
        .tab:hover { color:#555; }
        .tab.active { color:#7c3aed; border-bottom-color:#7c3aed; font-weight:600; }
        .tab-panel { display:none; flex:1; overflow-y:auto; padding:12px; flex-direction:column; }
        .tab-panel.active { display:flex; }

        /* ===== 编辑面板 ===== */
        .sel-box { font-size:13px; color:#555; padding:8px 10px; background:linear-gradient(135deg,#f8f9ff,#f3f0ff);
            border-radius:8px; margin-bottom:10px; border:1px solid #e8e0f7; line-height:1.5;
            word-break:break-all; min-height:28px; transition:all .2s; }
        .sel-box.has-text { border-color:#c4b5fd; background:linear-gradient(135deg,#f5f3ff,#ede9fe); }
        .sel-box .sel-label { font-size:11px; color:#a78bfa; font-weight:600; letter-spacing:.5px; }
        .custom-row { margin-bottom:8px; }
        .custom-row input { width:100%; padding:8px 10px; border:1px solid #e5e7eb; border-radius:6px;
            font-size:13px; color:#333; outline:none; transition:border-color .15s; background:#fafafa; }
        .custom-row input:focus { border-color:#7c3aed; background:#fff; box-shadow:0 0 0 2px rgba(124,58,237,.08); }
        .custom-row input::placeholder { color:#aaa; }

        /* 按钮 */
        .action-bar { display:flex; flex-wrap:wrap; gap:6px; margin-bottom:10px; }
        .pill { display:inline-flex; align-items:center; gap:4px; padding:7px 12px;
            border:1px solid #e5e7eb; border-radius:18px; background:#fff; cursor:pointer;
            font-size:13px; color:#555; transition:all .15s; white-space:nowrap; }
        .pill:hover { background:#f5f3ff; border-color:#c4b5fd; color:#7c3aed; }
        .pill:active { background:#ede9fe; transform:scale(.96); }
        .pill.disabled { opacity:.35; pointer-events:none; }
        .pill .pi { font-size:14px; }
        .pill.full-doc { border-color:#10b981; color:#059669; }
        .pill.full-doc:hover { background:#ecfdf5; border-color:#059669; }

        .status { display:none; padding:8px 10px; border-radius:6px; font-size:13px; margin-bottom:8px; }
        .status.info { display:block; background:#eff6ff; color:#2563eb; border:1px solid #bfdbfe; }
        .status.error { display:block; background:#fef2f2; color:#dc2626; border:1px solid #fecaca; }
        .status.success { display:block; background:#f0fdf4; color:#16a34a; border:1px solid #bbf7d0; }

        /* 结果展示区 */
        .result-box { display:none; border:1px solid #e5e7eb; border-radius:10px; margin-bottom:8px;
            overflow:hidden; box-shadow:0 1px 4px rgba(0,0,0,.04); flex:1; display:none; flex-direction:column; }
        .result-box.show { display:flex; animation:fadeIn .2s; }
        @keyframes fadeIn { from{opacity:0;transform:translateY(4px)} to{opacity:1;transform:translateY(0)} }
        .result-hd { background:linear-gradient(135deg,#faf5ff,#f3e8ff); padding:8px 12px;
            font-size:13px; color:#9333ea; border-bottom:1px solid #e9d5ff; font-weight:500;
            display:flex; justify-content:space-between; align-items:center; flex-shrink:0; }
        .result-hd .close-btn { cursor:pointer; opacity:.6; font-size:16px; }
        .result-hd .close-btn:hover { opacity:1; }
        .result-body { padding:10px 12px; font-size:13px; line-height:1.8; flex:1; overflow-y:auto; }
        .result-ft { display:flex; gap:6px; padding:8px 10px; border-top:1px solid #f3e8ff; background:#fdfaff;
            align-items:center; flex-wrap:wrap; flex-shrink:0; }
        .result-ft .spc { flex:1; }
        .rbtn { padding:6px 14px; border-radius:6px; font-size:13px; cursor:pointer; border:1px solid #e5e7eb;
            background:#fff; color:#555; transition:all .12s; }
        .rbtn:hover { background:#f5f5f5; }
        .rbtn.ok { background:#7c3aed; color:#fff; border-color:#7c3aed; }
        .rbtn.ok:hover { background:#6d28d9; }
        .applied-msg { color:#16a34a; font-size:13px; font-weight:500; }

        /* 检查结果样式 */
        .check-item { padding:10px; margin-bottom:8px; border-radius:8px; border-left:3px solid #e5e7eb; background:#f9fafb; }
        .check-item.error { border-left-color:#ef4444; background:#fef2f2; }
        .check-item.warning { border-left-color:#f59e0b; background:#fffbeb; }
        .check-item.info { border-left-color:#3b82f6; background:#eff6ff; }
        .check-item .check-type { font-size:11px; font-weight:600; margin-bottom:4px; }
        .check-item.error .check-type { color:#dc2626; }
        .check-item.warning .check-type { color:#d97706; }
        .check-item.info .check-type { color:#2563eb; }
        .check-item .check-original { color:#666; margin-bottom:4px; }
        .check-item .check-original .highlight { background:#fecaca; padding:1px 4px; border-radius:3px; }
        .check-item .check-suggest { color:#059669; }
        .check-item .check-suggest .highlight { background:#bbf7d0; padding:1px 4px; border-radius:3px; }
        .check-summary { padding:10px; background:#f0fdf4; border-radius:8px; margin-top:8px; font-size:13px; color:#166534; }
        .check-summary.has-error { background:#fef2f2; color:#991b1b; }

        /* Markdown 样式 */
        .md-h { margin:8px 0 4px; font-weight:600; }
        .md-code { background:#1e1e1e; color:#d4d4d4; padding:8px 10px; border-radius:6px; overflow-x:auto; font-size:12px; margin:6px 0; }
        .md-inline { background:#f3f4f6; padding:1px 4px; border-radius:3px; font-size:12px; color:#e11d48; }
        .md-list { margin:6px 0; padding-left:20px; }
        .md-list li { margin:2px 0; }
        .streaming-cursor { color:#7c3aed; animation:blink 1s infinite; }
        @keyframes blink { 0%,50%{opacity:1} 51%,100%{opacity:0} }
        .stream-content { line-height:1.8; }
        .stream-status { color:#666; font-size:12px; }

        /* 对照显示样式 */
        .compare-box { display:flex; flex-direction:column; gap:10px; height:100%; }
        .compare-section { border:1px solid #e5e7eb; border-radius:8px; overflow:hidden; flex:1; display:flex; flex-direction:column; min-height:0; }
        .compare-label { padding:6px 10px; font-size:12px; font-weight:600; background:#f9fafb; border-bottom:1px solid #e5e7eb; flex-shrink:0; }
        .compare-text { padding:10px; font-size:13px; line-height:1.7; flex:1; overflow-y:auto; white-space:pre-wrap; }
        .compare-text.original { background:#fef2f2; color:#991b1b; }
        .compare-text.edited { background:#f0fdf4; color:#166534; }
        .single-view { border:1px solid #e5e7eb; border-radius:8px; overflow:hidden; }

        /* 校对状态样式 */
        .check-status { padding:10px 12px; text-align:center; font-size:13px; background:#f8fafc; 
            border-radius:8px; margin-bottom:10px; }
        .check-status .check-found { color:#dc2626; font-weight:500; }
        .check-status .check-ok { color:#16a34a; }

        /* 错误列表样式 */
        .error-list { display:flex; flex-direction:column; gap:8px; }
        .error-item { display:flex; align-items:center; gap:10px; padding:10px 12px; 
            background:#fff; border:1px solid #e5e7eb; border-radius:10px; 
            transition:all .2s; box-shadow:0 1px 2px rgba(0,0,0,.04); }
        .error-item:hover { border-color:#c4b5fd; box-shadow:0 2px 8px rgba(124,58,237,.1); }
        .error-item.adopted { opacity:.5; background:#f0fdf4; border-color:#86efac; }
        
        .error-num { width:26px; height:26px; flex-shrink:0; display:flex; align-items:center; 
            justify-content:center; background:linear-gradient(135deg,#3b82f6,#2563eb); color:#fff; 
            border-radius:50%; font-size:12px; font-weight:600; }
        
        .error-body { flex:1; display:flex; align-items:center; gap:8px; flex-wrap:wrap; min-width:0; }
        
        .error-wrong { display:inline-block; padding:5px 12px; border-radius:6px; font-size:13px; font-weight:500;
            background:#FFE4E6; color:#be123c; text-decoration:line-through; text-decoration-color:#f87171; }
        
        .error-arrow { color:#9ca3af; font-size:16px; flex-shrink:0; }
        
        .error-correct { display:inline-block; padding:5px 12px; border-radius:6px; font-size:13px; font-weight:500;
            background:#dcfce7; color:#166534; }
        
        .error-btn { padding:6px 14px; border-radius:6px; border:none; font-size:12px; font-weight:500;
            background:linear-gradient(135deg,#3b82f6,#2563eb); color:#fff; cursor:pointer; 
            transition:all .15s; flex-shrink:0; }
        .error-btn:hover { transform:translateY(-1px); box-shadow:0 2px 6px rgba(59,130,246,.4); }
        .error-btn:active { transform:translateY(0); }
        .error-btn.adopted { background:#86efac; color:#166534; cursor:default; }
        .error-btn:disabled { opacity:.6; cursor:not-allowed; transform:none; box-shadow:none; }

        /* 总结内容样式 */
        .summary-content { line-height:1.8; font-size:13px; }
        .summary-content ul, .summary-content ol { padding-left:20px; margin:8px 0; }
        .summary-content li { margin:4px 0; }
</style>
</head>
<body>
    <div class="tabs">
        <div class="tab active" data-tab="edit">✏️ 编辑</div>
        <div class="tab" data-tab="chat">💬 对话</div>
    </div>

    <!-- 编辑面板 -->
    <div class="tab-panel active" id="panelEdit">
        <div class="sel-box" id="selInfo">💡 未选中文字时将对全文进行操作</div>
        <div id="statusBox" class="status"></div>
        <div class="custom-row">
            <input type="text" id="customInput" placeholder="可选:具体要求,如「改为正式公文风格」" />
        </div>
        <div class="action-bar" id="btnGrid"></div>
        <div class="result-box" id="resultBox">
            <div class="result-hd">
                <span id="resultHeader">结果</span>
                <span class="close-btn" onclick="hideResult()">✕</span>
            </div>
            <div class="result-body" id="resultContent"></div>
            <div class="result-ft" id="resultFooter">
                <div class="spc"></div>
                <button class="rbtn" onclick="hideResult()">关闭</button>
            </div>
        </div>
        <div class="dbg" id="debugInfo" style="font-size:10px;color:#ccc;margin-top:4px;"></div>
    </div>

    <!-- 对话面板 -->
    <div class="tab-panel" id="panelChat">
        <style>
            /* 对话面板样式 */
            .chat-status { font-size:11px; padding:4px 8px; border-radius:4px; margin-bottom:8px; text-align:center; }
            .chat-status.loaded { background:#ecfdf5; color:#059669; }
            .chat-status.empty { background:#fef3c7; color:#92400e; }
            
            .quick-actions { display:flex; flex-wrap:wrap; gap:6px; margin-bottom:10px; }
            .qa-btn { display:inline-flex; align-items:center; gap:4px; padding:6px 10px;
                border:1px solid #e5e7eb; border-radius:16px; font-size:12px; cursor:pointer;
                background:#fff; color:#555; transition:all .15s; }
            .qa-btn:hover { background:#f5f3ff; border-color:#c4b5fd; color:#7c3aed; }
            .qa-btn .qi { font-size:13px; }
            
            .chat-msgs { flex:1; overflow-y:auto; margin-bottom:8px; min-height:100px;
                background:#f9fafb; border-radius:8px; padding:8px; }
            .cm { padding:10px 12px; border-radius:10px; margin-bottom:8px; font-size:14px; line-height:1.7;
                word-break:break-word; max-width:90%; }
            .cm.user { background:linear-gradient(135deg,#7c3aed,#6d28d9); color:#fff; 
                margin-left:auto; border-bottom-right-radius:3px; }
            .cm.assistant { background:#fff; color:#333; border:1px solid #e5e7eb;
                border-bottom-left-radius:3px; box-shadow:0 1px 2px rgba(0,0,0,.04); }
            .cm code { background:rgba(0,0,0,.06); padding:2px 5px; border-radius:3px; font-size:13px; }
            .cm.user code { background:rgba(255,255,255,.2); }
            .cm pre { background:#1e1e1e; color:#d4d4d4; padding:10px; border-radius:6px; 
                overflow-x:auto; margin:8px 0; font-size:12px; }
            .cm strong { font-weight:600; }
            .cm ul, .cm ol { margin:6px 0; padding-left:20px; }
            .cm li { margin:3px 0; }
            
            .streaming-indicator { display:flex; align-items:center; gap:6px; padding:6px 10px;
                background:#fef3c7; border-radius:6px; font-size:12px; color:#92400e; margin-bottom:8px; }
            .streaming-indicator .dot { width:6px; height:6px; background:#f59e0b; border-radius:50%;
                animation:pulse 1s infinite; }
            @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.3} }
            
            .chat-input-row { display:flex; gap:6px; flex-shrink:0; }
            .chat-input-row textarea { flex:1; padding:10px 12px; border:1px solid #e5e7eb; border-radius:8px;
                font-size:14px; resize:none; outline:none; font-family:inherit; min-height:40px; }
            .chat-input-row textarea:focus { border-color:#7c3aed; box-shadow:0 0 0 2px rgba(124,58,237,.08); }
            .chat-input-row button { padding:10px 16px; border-radius:8px; border:none;
                background:#7c3aed; color:#fff; font-size:14px; cursor:pointer; white-space:nowrap; }
            .chat-input-row button:hover { background:#6d28d9; }
            .chat-input-row button:disabled { opacity:.5; cursor:not-allowed; }
        </style>
        
        <div class="chat-status" id="chatStatus">点击加载文档内容</div>
        
        <div class="quick-actions">
            <button class="qa-btn" onclick="quickChat('总结这份文档的主要内容')"><span class="qi">📋</span>总结</button>
            <button class="qa-btn" onclick="quickChat('提取文档中的关键信息')"><span class="qi">🔑</span>提取</button>
            <button class="qa-btn" onclick="quickChat('将文档翻译成英文')"><span class="qi">🌐</span>翻译</button>
            <button class="qa-btn" onclick="quickChat('分析文档的结构和逻辑')"><span class="qi">📊</span>分析</button>
        </div>
        
        <div class="chat-msgs" id="chatMsgs">
            <div class="cm assistant">你好!我是文档 AI 助手,已加载当前文档内容。你可以问我关于文档的任何问题。</div>
        </div>
        
        <div class="streaming-indicator" id="streamingIndicator" style="display:none">
            <div class="dot"></div>
            <span>AI 正在思考...</span>
        </div>
        
        <div class="chat-input-row">
            <textarea id="chatInput" rows="1" placeholder="输入问题..." 
                onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendChat()}"
                oninput="this.style.height='auto';this.style.height=Math.min(this.scrollHeight,80)+'px'"></textarea>
            <button id="chatSendBtn" onclick="sendChat()">发送</button>
        </div>
    </div>

    <script type="text/javascript">
        console.log("[HTML] Script loading started...");
        
        // 确保 DOM 完全就绪后再加载脚本
        function loadPluginScript() {
            console.log("[HTML] loadPluginScript() called");
            var script = document.createElement('script');
            script.type = 'text/javascript';
            script.src = 'scripts/code.js';
            script.onload = function() {
                console.log("[HTML] Plugin script loaded successfully");
                // 备用初始化:如果 Asc.plugin.init 没有自动被调用,手动触发
                setTimeout(function() {
                    if (window.Asc && window.Asc.plugin && typeof window.Asc.plugin.init === 'function') {
                        console.log("[HTML] Calling Asc.plugin.init manually as fallback");
                        window.Asc.plugin.init("");
                    }
                }, 100);
            };
            script.onerror = function() {
                console.error("[HTML] Failed to load plugin script");
            };
            document.body.appendChild(script);
            console.log("[HTML] Script element appended to body");
        }
        
        // 立即尝试加载,不等待 DOMContentLoaded
        if (document.readyState === 'complete' || document.readyState === 'interactive') {
            console.log("[HTML] DOM is already ready, loading script immediately");
            loadPluginScript();
        } else {
            console.log("[HTML] Waiting for DOMContentLoaded...");
            document.addEventListener('DOMContentLoaded', loadPluginScript);
            // 备用:如果 DOMContentLoaded 没有触发,500ms后强制加载
            setTimeout(loadPluginScript, 500);
        }
    </script>
</body>
</html>
javascript 复制代码
(function(window, undefined) {
    

    // MAX_TEXT_LENGTH: 单次处理的最大文本长度(字符数)
    // 超过此长度会提示用户选择更短的内容
    var MAX_TEXT_LENGTH = 8000;
    console.debug("[AI插件] 配置参数 - MAX_TEXT_LENGTH:", MAX_TEXT_LENGTH);

    // 选中文字操作(编辑功能)
    var SELECTION_ACTIONS = [
        { id: "polish",   icon: "✨", label: "润色",     prompt: "润色优化这段文字,保持原意不变,直接输出修改后的内容,不要任何解释" },
        { id: "rewrite",  icon: "🔄", label: "改写",     prompt: "改写这段文字,用不同的表达方式,直接输出修改后的内容,不要任何解释" },
        { id: "shorten",  icon: "✂️", label: "精简",     prompt: "精简这段文字,去除冗余保留核心内容,直接输出修改后的内容,不要任何解释" },
        { id: "expand",   icon: "📝", label: "扩写",     prompt: "扩写这段文字,补充更多细节和内容,直接输出修改后的内容,不要任何解释" },
        { id: "translate", icon: "🌐", label: "翻译英文", prompt: "将这段文字翻译成英文,直接输出翻译结果,不要任何解释" }
    ];

    // 全文操作
    var FULLTEXT_ACTIONS = [
        { id: "doccheck", icon: "🔍", label: "文档校对", checkType: "full" },
        { id: "summary", icon: "📊", label: "总结", checkType: "summary" }
    ];

    var _busy = false;
    var _selText = "";
    var _fullText = "";
    var _editedText = "";
    var _originalText = "";
    var _lastLabel = "";
    var _chatHistory = [];
    var _streamingText = "";
    var _docLoaded = false;
    var _checkErrors = [];
    var _textToCheck = "";
    var _hasSelection = false;
    var _pluginInitialized = false;

    /* ===== 配置和调试 ===== */
    var DEBUG_MODE = true;
    
    function logDebug(msg) {
        if (DEBUG_MODE) {
            var timestamp = new Date().toLocaleTimeString('zh-CN', { hour12: false });
            console.log("[AI插件][" + timestamp + "] " + msg);
        }
    }

    /* ===== DOM ===== */
    function $(id) { 
        logDebug("$('" + id + "') 被调用");
        var el = document.getElementById(id);
        logDebug("$('" + id + "') 返回: " + (el ? "找到" : "null"));
        return el;
    }

    function escapeHtml(str) {
        if (!str) return "";
        return String(str).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
    }

    function setStatus(msg, type) {
        var el = $("statusBox");
        if (!el) return;
        el.className = "status " + (type || "info");
        el.textContent = msg;
    }
    function clearStatus() {
        var el = $("statusBox");
        if (el) { el.className = "status"; el.textContent = ""; }
    }

    function setSelInfo(text) {
        logDebug("setSelInfo() 被调用, 文本长度: " + (text ? text.length : 0));
        _selText = text || "";
        var el = $("selInfo");
        if (!el) return;
        if (!_selText.trim()) {
            el.className = "sel-box";
            el.textContent = "💡 未选中文字时将对全文进行操作";
            _hasSelection = false;
        } else {
            el.className = "sel-box has-text";
            el.textContent = "已选中文字";
            _hasSelection = true;
        }
        logDebug("setSelInfo() 完成, _hasSelection: " + _hasSelection);
    }

    function updateBtnState() {
        logDebug("updateBtnState() 被调用, _busy: " + _busy);
        var btns = document.querySelectorAll(".pill");
        logDebug("updateBtnState() - 找到按钮数量: " + btns.length);
        for (var i = 0; i < btns.length; i++) {
            if (!_busy) btns[i].classList.remove("disabled");
            else btns[i].classList.add("disabled");
        }
        logDebug("updateBtnState() 完成");
    }

    function debug(msg) {
        console.log("[AI插件] " + msg);
    }

    /* ===== 获取选中文本 - 使用 GetSelectedText 方法 ===== */
    function getSelectedText(callback) {
        logDebug("[AI插件] getSelectedText - 开始获取选中文本");
        
        // 首先检查 window.Asc.plugin 是否可用
        if (typeof window === 'undefined' || typeof window.Asc === 'undefined' || typeof window.Asc.plugin === 'undefined') {
            logDebug("[AI插件] getSelectedText - window.Asc.plugin 不可用");
            callback("");
            return;
        }
        
        // 检查 executeMethod 是否可用
        if (typeof window.Asc.plugin.executeMethod !== 'function') {
            logDebug("[AI插件] getSelectedText - executeMethod 不可用,尝试其他方法");
            // 备选方案:直接读取 _selText 变量(可能在 onSelectionChanged 事件中已更新)
            logDebug("[AI插件] getSelectedText - 使用缓存的 _selText,长度: " + (_selText ? _selText.length : 0));
            callback(_selText || "");
            return;
        }
        
        try {
            logDebug("[AI插件] getSelectedText - 调用 window.Asc.plugin.executeMethod('GetSelectedText')");
            window.Asc.plugin.executeMethod('GetSelectedText', null, function(text) {
                logDebug("[AI插件] getSelectedText - 回调返回,文本类型: " + typeof text + ", 文本长度: " + (text ? text.length : 0));
                logDebug("[AI插件] getSelectedText - 回调返回的文本开头: " + (text ? text.substring(0, 50) + "..." : "空"));
                
                // 如果返回空,尝试使用之前缓存的选中文本
                if (!text || !text.trim()) {
                    logDebug("[AI插件] getSelectedText - 返回为空或空白,尝试使用缓存的 _selText");
                    logDebug("[AI插件] getSelectedText - 缓存的 _selText 长度: " + (_selText ? _selText.length : 0));
                    text = _selText || "";
                }
                
                logDebug("[AI插件] getSelectedText - 最终返回文本长度: " + text.length);
                callback(typeof text === 'string' ? text : "");
            });
        } catch(e) {
            console.error("[AI插件] getSelectedText 错误:", e);
            logDebug("[AI插件] getSelectedText - 捕获异常,使用缓存的 _selText");
            // 备选方案:使用缓存的文本
            callback(_selText || "");
        }
    }

    /* ===== 获取全文 ===== */
    // function getFullText(callback) {
    //     Asc.plugin.callCommand(function() {
    //         var doc = Api.GetDocument();
    //         logDebug("getFullText() - document found: " + (doc ? "found" : "null"));
    //         var text = "";
    //         var count = doc.GetElementsCount();
    //         logDebug("getFullText() - elements found: " + count);
    //         for (var i = 0; i < count; i++) {
    //             var el = doc.GetElement(i);
    //             if (el.GetClassType && el.GetClassType() === "paragraph") {
    //                 text += el.GetText() + "\n";
    //             }
    //         }
    //         return text;
    //     }, false, false, function(result) {
    //         console.log("getFullText() - result:", result);
    //         if (result && result.trim()) {
    //             _fullText = result;
    //         }
    //         if (callback) callback(_fullText);
    //     });
    // }

    function getFullText(callback) {
        logDebug("[AI插件] getFullText - 使用 callCommand 获取全文");
        try {
            Asc.plugin.callCommand(function() {
                try {
                    if (typeof Api === 'undefined' || !Api) {
                        return "";
                    }
                    
                    var oDocument = Api.GetDocument();
                    if (!oDocument) {
                        return "";
                    }
                    
                    var text = "";
                    
                    // 尝试使用 GetAllContent 方法
                    if (typeof oDocument.GetAllContent === 'function') {
                        try {
                            var aContent = oDocument.GetAllContent();
                            if (aContent && aContent.length > 0) {
                                for (var i = 0; i < aContent.length; i++) {
                                    var oElement = aContent[i];
                                    if (!oElement) continue;
                                    
                                    var sType = oElement.GetClassType ? oElement.GetClassType() : "unknown";
                                    
                                    if (sType === "paragraph" || sType === "heading" || sType === "textbox") {
                                        if (typeof oElement.GetText === 'function') {
                                            var sElementText = oElement.GetText();
                                            if (typeof sElementText === 'string') {
                                                text += sElementText + "\n";
                                            }
                                        }
                                    } else if (sType === "table") {
                                        text += getTableText(oElement) + "\n";
                                    } else if (typeof oElement.GetText === 'function') {
                                        var sOtherText = oElement.GetText();
                                        if (typeof sOtherText === 'string' && sOtherText.trim()) {
                                            text += sOtherText + "\n";
                                        }
                                    }
                                }
                                
                                if (text.trim()) {
                                    return text;
                                }
                            }
                        } catch(e) {
                        }
                    }
                    
                    // 备选方案:使用 GetElementsCount
                    var nCount = oDocument.GetElementsCount ? oDocument.GetElementsCount() : 0;
                    if (nCount > 0 && typeof oDocument.GetElement === 'function') {
                        for (var i = 0; i < nCount; i++) {
                            var oElement = oDocument.GetElement(i);
                            if (!oElement || !oElement.GetClassType) continue;
                            
                            var sType = oElement.GetClassType();
                            
                            if (sType === "paragraph" || sType === "heading" || sType === "textbox") {
                                if (typeof oElement.GetText === 'function') {
                                    var sElementText = oElement.GetText();
                                    if (typeof sElementText === 'string') {
                                        text += sElementText + "\n";
                                    }
                                }
                            } else if (sType === "table") {
                                text += getTableText(oElement) + "\n";
                            }
                        }
                    }
                    
                    return text;
                } catch(e) {
                    return "";
                }
            }, false, false, function(result) {
                if (typeof result === 'string') {
                    _fullText = result;
                    logDebug("[AI插件] getFullText - 获取到文本长度: " + result.length);
                }
                if (callback) callback(_fullText);
            });
        } catch(e) {
            console.error("[AI插件] getFullText 错误:", e);
            if (callback) callback(_fullText);
        }
    }
    
    /**
     * 获取表格文本内容(根据官方API优化)
     * @param {Object} oTable - 表格对象
     * @returns {string} - 表格文本内容
     */
    function getTableText(oTable) {
        var text = "";
        if (!oTable || typeof oTable.GetRowsCount !== 'function') {
            return text;
        }
        
        var nRowCount = oTable.GetRowsCount();
        logDebug("[AI插件] getTableText - 表格行数: " + nRowCount);
        
        for (var i = 0; i < nRowCount; i++) {
            var oRow = oTable.GetRow(i);
            if (!oRow || typeof oRow.GetCellsCount !== 'function') {
                continue;
            }
            
            var nCellCount = oRow.GetCellsCount();
            var rowText = "";
            
            for (var j = 0; j < nCellCount; j++) {
                var oCell = oRow.GetCell(j);
                if (oCell && typeof oCell.GetText === 'function') {
                    var sCellText = oCell.GetText();
                    if (typeof sCellText === 'string') {
                        rowText += sCellText + "\t";
                    }
                }
            }
            
            // 移除行尾多余的制表符
            rowText = rowText.replace(/\t$/, '');
            if (rowText.trim()) {
                text += rowText + "\n";
            }
        }
        
        return text;
    }
    


    /* ===== 标签页切换 ===== */
    function initTabs() {
        logDebug("initTabs() 被调用");
        var tabs = document.querySelectorAll(".tab");
        logDebug("initTabs() - 找到标签页数量: " + tabs.length);
        for (var i = 0; i < tabs.length; i++) {
            tabs[i].onclick = function() {
                switchTab(this.getAttribute("data-tab"));
            };
        }
        logDebug("initTabs() 完成");
    }

    function switchTab(tabName) {
        logDebug("switchTab('" + tabName + "') 被调用");
        var tabs = document.querySelectorAll(".tab");
        logDebug("switchTab - 找到标签页数量: " + tabs.length);
        for (var i = 0; i < tabs.length; i++) {
            tabs[i].classList.toggle("active", tabs[i].getAttribute("data-tab") === tabName);
        }
        var panels = document.querySelectorAll(".tab-panel");
        logDebug("switchTab - 找到面板数量: " + panels.length);
        for (var j = 0; j < panels.length; j++) {
            panels[j].classList.toggle("active", panels[j].id === "panel" + tabName.charAt(0).toUpperCase() + tabName.slice(1));
        }
        if (tabName === "chat" && !_docLoaded) {
            logDebug("switchTab - 正在加载文档用于聊天");
            loadDocForChat();
        }
        logDebug("switchTab('" + tabName + "') 完成");
    }

    /* ===== 渲染按钮 ===== */
    var _renderRetryCount = 0;
    var MAX_RENDER_RETRIES = 20; // 最多重试 20 次,约 2 秒
    
    function renderButtons() {
        logDebug("renderButtons() 被调用, 尝试次数: " + (_renderRetryCount + 1));
        var grid = $("btnGrid");
        if (!grid) {
            _renderRetryCount++;
            if (_renderRetryCount >= MAX_RENDER_RETRIES) {
                logDebug("renderButtons() - #btnGrid 元素在 " + MAX_RENDER_RETRIES + " 次重试后仍未找到!");
                return;
            }
            logDebug("renderButtons() - #btnGrid 元素未找到,重试中...");
            setTimeout(renderButtons, 100);
            return;
        }
        
        logDebug("renderButtons() - #btnGrid 已找到,正在渲染按钮...");
        var html = "";
        
        for (var i = 0; i < SELECTION_ACTIONS.length; i++) {
            var a = SELECTION_ACTIONS[i];
            html += '<button class="pill" data-action="selection" data-idx="' + i + '">'
                  + '<span class="pi">' + a.icon + '</span>' + a.label + '</button>';
        }
        
        for (var j = 0; j < FULLTEXT_ACTIONS.length; j++) {
            var b = FULLTEXT_ACTIONS[j];
            html += '<button class="pill full-doc" data-action="fulltext" data-idx="' + j + '">'
                  + '<span class="pi">' + b.icon + '</span>' + b.label + '</button>';
        }
        
        grid.innerHTML = html;
        logDebug("renderButtons() - 按钮渲染成功");
        bindButtonEvents();
    }

    function bindButtonEvents() {
        logDebug("bindButtonEvents() 被调用");
        var btns = document.querySelectorAll("#btnGrid .pill");
        logDebug("bindButtonEvents() - 找到按钮数量: " + btns.length);
        for (var k = 0; k < btns.length; k++) {
            (function(btn) {
                btn.onclick = function() {
                    if (_busy) {
                        logDebug("bindButtonEvents() - 按钮点击被忽略,因为 _busy = true");
                        return;
                    }
                    
                    logDebug("bindButtonEvents() - 按钮被点击");
                    logDebug("bindButtonEvents() - Asc.plugin 是否可用: " + (typeof Asc !== 'undefined' && typeof Asc.plugin !== 'undefined'));
                    logDebug("bindButtonEvents() - 当前 _selText 长度: " + (_selText ? _selText.length : 0));
                    logDebug("bindButtonEvents() - 当前 _hasSelection: " + _hasSelection);
                    
                    var actionType = btn.getAttribute("data-action");
                    var idx = parseInt(btn.getAttribute("data-idx"), 10);
                    logDebug("bindButtonEvents() - actionType: " + actionType + ", idx: " + idx);
                    
                    if (actionType === "selection") {
                        doSelectionAI(SELECTION_ACTIONS[idx]);
                    } else {
                        doFullTextAI(FULLTEXT_ACTIONS[idx]);
                    }
                };
            })(btns[k]);
        }
        logDebug("bindButtonEvents() 完成");
    }

    /* ===== 结果显示 ===== */
    function showResultBox() {
        var box = $("resultBox");
        if (box) box.classList.add("show");
    }
    function hideResult() {
        var box = $("resultBox");
        if (box) box.classList.remove("show");
        clearStatus();
    }
    window.hideResult = hideResult;

    /* ===== 选中文字 AI 操作(润色、改写等) ===== */
    function doSelectionAI(action) {
        if (_busy) return;
        
        getSelectedText(function(selText) {
            debug("获取到选中文本: " + (selText ? selText.length + "字" : "无"));
            
            if (!selText || !selText.trim()) {
                getFullText(function(fullText) {
                    if (!fullText || !fullText.trim()) {
                        setStatus("请先选中文字或确保文档有内容", "error");
                        return;
                    }
                    processSelectionAI(action, fullText, false);
                });
            } else {
                processSelectionAI(action, selText, true);
            }
        });
    }

    function processSelectionAI(action, text, hasSelection) {
        if (text.length > MAX_TEXT_LENGTH) {
            setStatus("文本过长(最多" + MAX_TEXT_LENGTH + "字),请选择较短的内容", "error");
            return;
        }
        
        _originalText = text;
        _selText = hasSelection ? text : "";
        _hasSelection = hasSelection;
        _lastLabel = action.label;
        _busy = true;
        updateBtnState();
        setStatus(action.label + "中...", "info");
        
        var customInput = $("customInput");
        var customReq = customInput ? customInput.value.trim() : "";
        var prompt = action.prompt;
        if (customReq) {
            prompt += ",额外要求:" + customReq;
        }
        
        doSelectionStream(text, prompt, action.label);
    }

    function doSelectionStream(text, prompt, label) {
        _editedText = "";
        _streamingText = "";
        
        var header = $("resultHeader");
        var content = $("resultContent");
        var footer = $("resultFooter");
        
        if (header) header.textContent = label + " 结果";
        if (content) {
            content.innerHTML = '<div class="compare-box" style="display:block;max-height:400px;overflow-y:auto;padding-right:8px;">' +
                '<div class="compare-section" style="display:block;border:1px solid #e5e7eb;border-radius:8px;overflow:hidden;margin-bottom:8px;">' +
                '<div class="compare-label" style="padding:6px 10px;font-size:12px;font-weight:600;background:#f9fafb;border-bottom:1px solid #e5e7eb;">📄 原文</div>' +
                '<div class="compare-text original" style="padding:10px;font-size:13px;line-height:1.7;white-space:pre-wrap;background:#fef2f2;color:#991b1b;max-height:200px;overflow-y:auto;">' + escapeHtml(text) + '</div></div>' +
                '<div class="compare-section" style="display:block;border:1px solid #e5e7eb;border-radius:8px;overflow:hidden;">' +
                '<div class="compare-label" style="padding:6px 10px;font-size:12px;font-weight:600;background:#f9fafb;border-bottom:1px solid #e5e7eb;">✨ ' + label + '后</div>' +
                '<div class="compare-text edited" id="streamOutput" style="padding:10px;font-size:13px;line-height:1.7;white-space:pre-wrap;background:#f0fdf4;color:#166534;min-height:100px;max-height:200px;overflow-y:auto;"><span class="streaming-cursor" style="color:#7c3aed;">▌</span></div></div></div>';
        }
        if (footer) {
            footer.innerHTML = '<div class="stream-status">正在生成...</div><div class="spc"></div>';
        }
        showResultBox();
        
        // 构建API请求URL
        var url = API_BASE + "ai/chat";
        console.debug("[AI插件] doSelectionStream - 请求URL:", url);
        
        // 构建请求消息体
        var messages = [{ role: "user", content: prompt + "\n\n" + text }];
        console.debug("[AI插件] doSelectionStream - 请求消息:", JSON.stringify(messages, null, 2));
        
        // 获取认证Token并构建请求头
        var token = getAccessToken();
        var headers = { "Content-Type": "application/json" };
        if (token) {
            headers.Authorization = "Bearer " + token;
            console.debug("[AI插件] doSelectionStream - 使用Token认证");
        } else {
            console.warn("[AI插件] doSelectionStream - 未找到access_token,请求将不携带认证信息");
        }
        console.debug("[AI插件] doSelectionStream - 请求头:", headers);
        
        // 构建完整请求体
        var requestBody = { 
            user: USER, 
            messages: messages, 
            action: "chat" 
        };
        console.debug("[AI插件] doSelectionStream - 请求体:", JSON.stringify(requestBody, null, 2));
        
        // 发起API请求(流式响应)
        console.debug("[AI插件] doSelectionStream - 开始发起POST请求");
        fetch(url, {
            method: "POST",
            headers: headers,
            body: JSON.stringify(requestBody)
        }).then(function(resp) {
            console.debug("[AI插件] doSelectionStream - 响应状态码:", resp.status, resp.statusText);
            if (!resp.ok) throw new Error("请求失败: " + resp.status);
            
            // 处理流式响应
            var reader = resp.body.getReader();
            var decoder = new TextDecoder();
            console.debug("[AI插件] doSelectionStream - 开始接收流式响应");
            
            function read() {
                reader.read().then(function(result) {
                    if (result.done) {
                        console.debug("[AI插件] doSelectionStream - 流式响应接收完成");
                        finishSelectionStream(label);
                        return;
                    }
                    var chunk = decoder.decode(result.value, { stream: true });
                    console.debug("[AI插件] doSelectionStream - 接收到数据块,长度:", chunk.length);
                    processSSEChunk(chunk, function(token) {
                        _streamingText += token;
                        console.debug("[AI插件] doSelectionStream - _streamingText长度:", _streamingText.length);
                        var el = $("streamOutput");
                        console.debug("[AI插件] doSelectionStream - streamOutput元素:", el);
                        if (el) {
                            console.debug("[AI插件] doSelectionStream - 更新元素内容,长度:", _streamingText.length);
                            el.innerHTML = escapeHtml(_streamingText) + '<span class="streaming-cursor">▌</span>';
                        } else {
                            console.error("[AI插件] doSelectionStream - 未找到streamOutput元素!");
                        }
                    });
                    read();
                }).catch(function(err) {
                    console.error("[AI插件] doSelectionStream - 流式读取错误:", err);
                    finishSelectionStream(label);
                });
            }
            read();
        }).catch(function(err) {
            console.error("[AI插件] doSelectionStream - 请求失败:", err.message);
            _busy = false;
            updateBtnState();
            setStatus("请求失败: " + err.message, "error");
        });
    }

    function finishSelectionStream(label) {
        _busy = false;
        updateBtnState();
        clearStatus();
        _editedText = _streamingText;
        
        var el = $("streamOutput");
        if (el) el.innerHTML = escapeHtml(_editedText);
        
        var footer = $("resultFooter");
        if (footer) {
            footer.innerHTML = '<button class="rbtn" onclick="undoEdit()">撤销</button>' +
                '<div class="spc"></div>' +
                '<button class="rbtn ok" onclick="applyResult()">确认修改</button>';
        }
    }

    function processSSEChunk(chunk, onToken) {
        console.debug("[AI插件] processSSEChunk - 原始数据块:", chunk);
        var lines = chunk.split("\n");
        console.debug("[AI插件] processSSEChunk - 分割后行数:", lines.length);
        
        for (var i = 0; i < lines.length; i++) {
            var line = lines[i].trim();
            console.debug("[AI插件] processSSEChunk - 处理行[" + i + "]:", line);
            
            if (line.startsWith("data: ")) {
                var dataContent = line.substring(6);
                
                // 方案1:尝试解析为 JSON 格式(type + content)
                try {
                    var data = JSON.parse(dataContent);
                    console.debug("[AI插件] processSSEChunk - 解析为JSON:", data);
                    
                    if (data.type === "token" && data.content) {
                        console.debug("[AI插件] processSSEChunk - 格式1:JSON token格式,内容:", data.content);
                        onToken(data.content);
                    } else if (data.content) {
                        // 兼容没有type字段的情况
                        console.debug("[AI插件] processSSEChunk - 格式2:只有content字段,内容:", data.content);
                        onToken(data.content);
                    } else {
                        // 尝试直接使用data作为内容(如果是字符串)
                        if (typeof data === 'string') {
                            console.debug("[AI插件] processSSEChunk - 格式3:JSON字符串直接作为内容:", data);
                            onToken(data);
                        } else {
                            console.debug("[AI插件] processSSEChunk - 数据不匹配,type:", data.type, ", content:", data.content);
                        }
                    }
                } catch(e) {
                    // 方案2:JSON解析失败,直接使用文本内容
                    console.debug("[AI插件] processSSEChunk - JSON解析失败,尝试作为纯文本处理:", dataContent);
                    if (dataContent && dataContent !== "[DONE]" && dataContent !== "done" && dataContent !== "null") {
                        console.debug("[AI插件] processSSEChunk - 格式4:纯文本格式,内容:", dataContent);
                        onToken(dataContent);
                    } else {
                        console.debug("[AI插件] processSSEChunk - 忽略结束标记或空内容:", dataContent);
                    }
                }
            }
        }
    }

    /* ===== 全文操作(文档校对、总结) ===== */
    function doFullTextAI(action) {
        if (_busy) return;
        
        getSelectedText(function(selText) {
            debug("校对/总结 - 获取到选中文本: " + (selText ? selText.length + "字" : "无"));
            
            if (selText && selText.trim()) {
                processFullTextAI(action, selText, true);
            } else {
                getFullText(function(fullText) {
                    if (!fullText || !fullText.trim()) {
                        setStatus("请先选中文字或确保文档有内容", "error");
                        return;
                    }
                    processFullTextAI(action, fullText, false);
                });
            }
        });
    }

    function processFullTextAI(action, text, hasSelection) {
        _selText = hasSelection ? text : "";
        _hasSelection = hasSelection;
        _busy = true;
        updateBtnState();
        
        if (action.checkType === "summary") {
            setStatus("正在总结" + (hasSelection ? "选中内容" : "全文") + "...", "info");
            doSummaryStream(text);
        } else {
            setStatus("正在校对" + (hasSelection ? "选中内容" : "全文") + "...", "info");
            _textToCheck = text;
            doCheckStream(text);
        }
    }

    /**
     * 流式总结 - 调用AI对文本进行总结
     * @param {string} text - 待总结的文本内容
     */
    function doSummaryStream(text) {
        console.debug("[AI插件] doSummaryStream - 开始执行总结,文本长度:", text.length);
        _streamingText = "";
        
        var header = $("resultHeader");
        var content = $("resultContent");
        var footer = $("resultFooter");
        
        if (header) header.textContent = "📊 总结";
        if (content) {
            content.innerHTML = '<div class="summary-content" id="summaryOutput"><span class="streaming-cursor">▌</span></div>';
        }
        if (footer) {
            footer.innerHTML = '<div class="stream-status">正在生成...</div><div class="spc"></div>';
        }
        showResultBox();
        
        // 构建API请求URL
        var url = API_BASE + "ai/chat";
        console.debug("[AI插件] doSummaryStream - 请求URL:", url);
        
        // 构建总结请求消息
        var messages = [{ role: "user", content: "请对以下内容进行全面总结,概括主要内容和核心观点,提取关键信息:\n\n" + text }];
        console.debug("[AI插件] doSummaryStream - 请求消息:", JSON.stringify(messages, null, 2));
        
        // 获取认证Token
        var token = getAccessToken();
        var headers = { "Content-Type": "application/json" };
        if (token) {
            headers.Authorization = "Bearer " + token;
            console.debug("[AI插件] doSummaryStream - 使用Token认证");
        } else {
            console.warn("[AI插件] doSummaryStream - 未找到access_token");
        }
        
        // 构建请求体
        var requestBody = { user: USER, messages: messages, action: "chat" };
        console.debug("[AI插件] doSummaryStream - 请求体:", JSON.stringify(requestBody, null, 2));
        
        // 发起流式请求
        console.debug("[AI插件] doSummaryStream - 发起POST请求");
        fetch(url, {
            method: "POST",
            headers: headers,
            body: JSON.stringify(requestBody)
        }).then(function(resp) {
            console.debug("[AI插件] doSummaryStream - 响应状态:", resp.status);
            if (!resp.ok) throw new Error("请求失败: " + resp.status);
            
            var reader = resp.body.getReader();
            var decoder = new TextDecoder();
            console.debug("[AI插件] doSummaryStream - 开始接收流式响应");
            
            function read() {
                reader.read().then(function(result) {
                    if (result.done) {
                        console.debug("[AI插件] doSummaryStream - 流式响应完成");
                        finishSummaryStream();
                        return;
                    }
                    var chunk = decoder.decode(result.value, { stream: true });
                    processSSEChunk(chunk, function(token) {
                        _streamingText += token;
                        var el = $("summaryOutput");
                        if (el) el.innerHTML = renderMarkdown(_streamingText) + '<span class="streaming-cursor">▌</span>';
                    });
                    read();
                }).catch(function(err) {
                    console.error("[AI插件] 流式错误:", err);
                    finishSummaryStream();
                });
            }
            read();
        }).catch(function(err) {
            _busy = false;
            updateBtnState();
            setStatus("请求失败: " + err.message, "error");
        });
    }

    function finishSummaryStream() {
        _busy = false;
        updateBtnState();
        clearStatus();
        
        var el = $("summaryOutput");
        if (el) el.innerHTML = renderMarkdown(_streamingText);
        
        var footer = $("resultFooter");
        if (footer) {
            footer.innerHTML = '<div class="spc"></div><button class="rbtn" onclick="hideResult()">关闭</button>';
        }
    }

    /**
     * 流式校对 - 调用AI对文本进行错别字和病句检查
     * @param {string} text - 待校对的文本内容
     */
    function doCheckStream(text) {
        console.debug("[AI插件] doCheckStream - 开始执行校对,文本长度:", text.length);
        _checkErrors = [];
        _streamingText = "";
        
        var header = $("resultHeader");
        var content = $("resultContent");
        var footer = $("resultFooter");
        
        if (header) header.textContent = "🔍 文档校对";
        if (content) {
            content.innerHTML = '<div class="check-status" id="checkStatus">正在检查...</div>' +
                '<div class="error-list" id="errorList"></div>';
        }
        if (footer) {
            footer.innerHTML = '<div class="stream-status" id="checkProgress">检查中...</div><div class="spc"></div>';
        }
        showResultBox();
        
        // 构建API请求URL
        var url = API_BASE + "ai/chat";
        console.debug("[AI插件] doCheckStream - 请求URL:", url);
        
        // 构建校对提示词(包含详细的输出格式要求)
        var prompt = '你是一个专业的中文校对专家。请仔细检查以下文本中的错别字和病句。\n\n' +
            '重要规则:\n' +
            '1. 只输出你100%确定的错误,不确定的不要输出\n' +
            '2. 每个错误单独一行,格式:错误内容||正确内容\n' +
            '3. 错误内容必须是原文中实际存在的文字\n' +
            '4. 不要输出任何解释或说明\n' +
            '5. 如果没有发现明确的错误,只输出:无错误\n\n' +
            '示例输出格式:\n' +
            '因该||应该\n' +
            '他门||他们\n\n' +
            '待检查文本:\n' + text;
        console.debug("[AI插件] doCheckStream - 校对提示词构建完成");
        
        var messages = [{ role: "user", content: prompt }];
        console.debug("[AI插件] doCheckStream - 请求消息:", JSON.stringify(messages, null, 2));
        
        // 获取认证Token
        var token = getAccessToken();
        var headers = { "Content-Type": "application/json" };
        if (token) {
            headers.Authorization = "Bearer " + token;
            console.debug("[AI插件] doCheckStream - 使用Token认证");
        } else {
            console.warn("[AI插件] doCheckStream - 未找到access_token");
        }
        
        // 构建请求体
        var requestBody = { user: USER, messages: messages, action: "chat" };
        console.debug("[AI插件] doCheckStream - 请求体:", JSON.stringify(requestBody, null, 2));
        
        // 发起流式请求
        console.debug("[AI插件] doCheckStream - 发起POST请求");
        fetch(url, {
            method: "POST",
            headers: headers,
            body: JSON.stringify(requestBody)
        }).then(function(resp) {
            console.debug("[AI插件] doCheckStream - 响应状态:", resp.status);
            if (!resp.ok) throw new Error("请求失败: " + resp.status);
            
            var reader = resp.body.getReader();
            var decoder = new TextDecoder();
            console.debug("[AI插件] doCheckStream - 开始接收流式响应");
            
            function read() {
                reader.read().then(function(result) {
                    if (result.done) {
                        console.debug("[AI插件] doCheckStream - 流式响应完成");
                        finishCheckStream();
                        return;
                    }
                    var chunk = decoder.decode(result.value, { stream: true });
                    console.debug("[AI插件] doCheckStream - 接收到数据块,长度:", chunk.length);
                    processSSEChunk(chunk, function(token) {
                        _streamingText += token;
                        parseCheckErrors(_streamingText);
                    });
                    read();
                }).catch(function(err) {
                    console.error("[AI插件] doCheckStream - 流式读取错误:", err);
                    finishCheckStream();
                });
            }
            read();
        }).catch(function(err) {
            console.error("[AI插件] doCheckStream - 请求失败:", err.message);
            _busy = false;
            updateBtnState();
            setStatus("请求失败: " + err.message, "error");
        });
    }

    /**
     * 解析校对错误结果
     * @param {string} text - 流式返回的校对结果文本
     */
    function parseCheckErrors(text) {
        var lines = text.split('\n');
        var newErrors = [];
        
        for (var i = 0; i < lines.length; i++) {
            var line = lines[i].trim();
            if (!line || line === '无错误') continue;
            
            var parts = line.split('||');
            if (parts.length === 2) {
                var original = parts[0].trim();
                var corrected = parts[1].trim();
                
                if (original && corrected && original !== corrected && _textToCheck.indexOf(original) !== -1) {
                    var exists = false;
                    for (var j = 0; j < newErrors.length; j++) {
                        if (newErrors[j].original === original) {
                            exists = true;
                            break;
                        }
                    }
                    if (!exists) {
                        newErrors.push({ original: original, corrected: corrected });
                    }
                }
            }
        }
        
        if (newErrors.length !== _checkErrors.length) {
            _checkErrors = newErrors;
            renderCheckErrors();
        }
        
        var status = $("checkStatus");
        if (status) {
            if (_checkErrors.length > 0) {
                status.innerHTML = '<span class="check-found">已发现 ' + _checkErrors.length + ' 处问题</span>';
            } else {
                status.textContent = '正在检查...';
            }
        }
    }

    function renderCheckErrors() {
        var container = $("errorList");
        if (!container) return;
        
        var html = '';
        for (var i = 0; i < _checkErrors.length; i++) {
            var err = _checkErrors[i];
            html += '<div class="error-item" id="error-' + i + '">' +
                '<div class="error-num">' + (i + 1) + '</div>' +
                '<div class="error-body">' +
                '<span class="error-wrong">' + escapeHtml(err.original) + '</span>' +
                '<span class="error-arrow">→</span>' +
                '<span class="error-correct">' + escapeHtml(err.corrected) + '</span>' +
                '</div>' +
                '<button class="error-btn" onclick="adoptError(' + i + ')">采纳</button>' +
                '</div>';
        }
        container.innerHTML = html;
    }

    function finishCheckStream() {
        _busy = false;
        updateBtnState();
        clearStatus();
        
        var status = $("checkStatus");
        if (status) {
            if (_checkErrors.length === 0) {
                status.innerHTML = '<span class="check-ok">✅ 未发现明确错误,文档质量良好</span>';
            } else {
                status.innerHTML = '<span class="check-found">共发现 ' + _checkErrors.length + ' 处问题</span>';
            }
        }
        
        var footer = $("resultFooter");
        if (footer) {
            if (_checkErrors.length > 0) {
                footer.innerHTML = '<button class="rbtn" onclick="adoptAllErrors()">全部采纳</button>' +
                    '<div class="spc"></div>' +
                    '<button class="rbtn" onclick="hideResult()">关闭</button>';
            } else {
                footer.innerHTML = '<div class="spc"></div><button class="rbtn" onclick="hideResult()">关闭</button>';
            }
        }
    }

    // 采纳错误 - 使用搜索替换
    function adoptError(idx) {
        if (idx < 0 || idx >= _checkErrors.length) return;
        var err = _checkErrors[idx];
        
        debug("采纳错误 #" + (idx + 1) + ": " + err.original + " -> " + err.corrected);
        
        Asc.plugin.executeMethod("SearchAndReplace", [{
            searchString: err.original,
            replaceString: err.corrected,
            matchCase: true
        }], function(result) {
            var item = $("error-" + idx);
            if (item) {
                item.classList.add("adopted");
                var btn = item.querySelector(".error-btn");
                if (btn) {
                    btn.textContent = "✓ 已采纳";
                    btn.disabled = true;
                    btn.classList.add("adopted");
                }
            }
            setStatus("已采纳修正 #" + (idx + 1), "success");
            setTimeout(clearStatus, 2000);
        });
    }
    window.adoptError = adoptError;

    function adoptAllErrors() {
        if (_checkErrors.length === 0) return;
        
        var idx = 0;
        function adoptNext() {
            if (idx >= _checkErrors.length) {
                setStatus("已采纳全部 " + _checkErrors.length + " 处修正", "success");
                return;
            }
            var item = $("error-" + idx);
            if (item && !item.classList.contains("adopted")) {
                adoptError(idx);
            }
            idx++;
            setTimeout(adoptNext, 500);
        }
        adoptNext();
    }
    window.adoptAllErrors = adoptAllErrors;


    /* ===== 应用/撤销编辑 ===== */
    function applyResult() {
        if (!_editedText) {
            setStatus("没有可应用的内容", "error");
            return;
        }
        
        if (_hasSelection) {
            Asc.plugin.executeMethod("PasteText", [_editedText], function() {
                setStatus("已应用" + _lastLabel + "结果", "success");
                var footer = $("resultFooter");
                if (footer) {
                    footer.innerHTML = '<div class="applied-msg">✅ 已应用到文档</div><div class="spc"></div>' +
                        '<button class="rbtn" onclick="hideResult()">关闭</button>';
                }
            });
        } else {
            Asc.plugin.executeMethod("SelectAll", [], function() {
                Asc.plugin.executeMethod("PasteText", [_editedText], function() {
                    setStatus("已应用" + _lastLabel + "结果到全文", "success");
                    var footer = $("resultFooter");
                    if (footer) {
                        footer.innerHTML = '<div class="applied-msg">✅ 已应用到文档</div><div class="spc"></div>' +
                            '<button class="rbtn" onclick="hideResult()">关闭</button>';
                    }
                });
            });
        }
    }
    window.applyResult = applyResult;

    function undoEdit() {
        hideResult();
        setStatus("已撤销", "info");
        setTimeout(clearStatus, 1500);
    }
    window.undoEdit = undoEdit;

    /* ===== 对话功能 ===== */
    var _updatingChatStatus = false; // 防止重复调用
    
    function loadDocForChat() {
        logDebug("loadDocForChat() 被调用, _docLoaded: " + _docLoaded);
        if (_docLoaded) {
            logDebug("loadDocForChat() - 文档已加载,跳过");
            return;
        }
        getFullText(function(text) {
            logDebug("loadDocForChat() - getFullText 回调, 文本长度: " + (text ? text.length : 0));
            if (text && text.trim()) {
                _fullText = text;
            }
            _docLoaded = true;
            updateChatStatus();
        });
    }

    function updateChatStatus() {
        if (_updatingChatStatus) {
            logDebug("updateChatStatus() - 正在更新中,跳过");
            return;
        }
        _updatingChatStatus = true;
        
        try {
            logDebug("updateChatStatus() 被调用, _fullText.length: " + (_fullText ? _fullText.length : 0));
            var el = $("chatStatus");
            if (!el) {
                logDebug("updateChatStatus() chatStatus 元素未找到,返回");
                return;
            }
            if (_fullText && _fullText.trim()) {
                el.className = "chat-status loaded";
                el.textContent = "✅ 已加载文档(" + _fullText.length + " 字)";
                logDebug("updateChatStatus() 状态设置为已加载");
            } else {
                el.className = "chat-status empty";
                el.textContent = "⚠️ 文档为空,请先添加内容";
                logDebug("updateChatStatus() 状态设置为空");
            }
            logDebug("updateChatStatus() 完成");
        } finally {
            _updatingChatStatus = false;
        }
    }

    function quickChat(question) {
        var input = $("chatInput");
        if (input) {
            input.value = question;
            sendChat();
        }
    }
    window.quickChat = quickChat;

    /**
     * 发送聊天消息 - 用户与AI助手对话
     * 构建包含文档上下文的对话请求,发送到后端API
     */
    function sendChat() {
        console.debug("[AI插件] sendChat - 开始执行");
        
        var input = $("chatInput");
        var sendBtn = $("chatSendBtn");
        if (!input) {
            console.error("[AI插件] sendChat - 未找到chatInput元素");
            return;
        }
        
        var msg = input.value.trim();
        if (!msg) {
            console.warn("[AI插件] sendChat - 消息内容为空");
            return;
        }
        if (_busy) {
            console.warn("[AI插件] sendChat - 系统正忙,忽略请求");
            return;
        }
        
        // 更新状态
        _busy = true;
        if (sendBtn) sendBtn.disabled = true;
        input.value = "";
        input.style.height = "auto";
        
        // 添加用户消息到界面和历史记录
        addChatMessage("user", msg);
        _chatHistory.push({ role: "user", content: msg });
        console.debug("[AI插件] sendChat - 用户消息:", msg);
        console.debug("[AI插件] sendChat - 对话历史长度:", _chatHistory.length);
        
        // 显示加载指示器
        var indicator = $("streamingIndicator");
        if (indicator) indicator.style.display = "flex";
        
        // 创建助手消息占位
        var assistantMsgId = "assistant-" + Date.now();
        addChatMessage("assistant", "", assistantMsgId);
        
        // 构建系统消息(包含文档上下文)
        var systemMsg = "你是一个专业的文档 AI 助手。";
        if (_fullText && _fullText.trim()) {
            systemMsg += "\n\n当前文档内容:\n" + _fullText.substring(0, 8000);
            if (_fullText.length > 8000) systemMsg += "\n...(内容过长已截断)";
        }
        console.debug("[AI插件] sendChat - 系统消息长度:", systemMsg.length);
        
        // 构建完整消息列表
        var messages = [{ role: "system", content: systemMsg }].concat(_chatHistory);
        console.debug("[AI插件] sendChat - 消息列表数量:", messages.length);
        
        // 构建请求URL
        var url = API_BASE + "ai/chat";
        console.debug("[AI插件] sendChat - 请求URL:", url);
        _streamingText = "";
        
        // 获取认证Token
        var token = getAccessToken();
        var headers = { "Content-Type": "application/json" };
        if (token) {
            headers.Authorization = "Bearer " + token;
            console.debug("[AI插件] sendChat - 使用Token认证");
        } else {
            console.warn("[AI插件] sendChat - 未找到access_token");
        }
        
        // 构建请求体
        var requestBody = { user: USER, messages: messages, action: "chat" };
        console.debug("[AI插件] sendChat - 请求体:", JSON.stringify(requestBody, null, 2));
        
        // 发起流式请求
        console.debug("[AI插件] sendChat - 发起POST请求");
        fetch(url, {
            method: "POST",
            headers: headers,
            body: JSON.stringify(requestBody)
        }).then(function(resp) {
            console.debug("[AI插件] sendChat - 响应状态:", resp.status);
            if (!resp.ok) throw new Error("请求失败: " + resp.status);
            
            var reader = resp.body.getReader();
            var decoder = new TextDecoder();
            console.debug("[AI插件] sendChat - 开始接收流式响应");
            
            function read() {
                reader.read().then(function(result) {
                    if (result.done) {
                        console.debug("[AI插件] sendChat - 流式响应完成");
                        finishChat(assistantMsgId);
                        return;
                    }
                    var chunk = decoder.decode(result.value, { stream: true });
                    console.debug("[AI插件] sendChat - 接收到数据块,长度:", chunk.length);
                    processSSEChunk(chunk, function(token) {
                        _streamingText += token;
                        updateChatMessage(assistantMsgId, _streamingText);
                    });
                    read();
                }).catch(function(err) {
                    console.error("[AI插件] sendChat - 流式读取错误:", err);
                    finishChat(assistantMsgId);
                });
            }
            read();
        }).catch(function(err) {
            console.error("[AI插件] sendChat - 请求失败:", err.message);
            _busy = false;
            if (sendBtn) sendBtn.disabled = false;
            if (indicator) indicator.style.display = "none";
            updateChatMessage(assistantMsgId, "❌ 请求失败: " + err.message);
        });
    }
    window.sendChat = sendChat;

    function addChatMessage(role, content, id) {
        var container = $("chatMsgs");
        if (!container) return;
        
        var div = document.createElement("div");
        div.className = "cm " + role;
        if (id) div.id = id;
        div.innerHTML = role === "assistant" ? (renderMarkdown(content) || '<span class="streaming-cursor">▌</span>') : escapeHtml(content);
        container.appendChild(div);
        container.scrollTop = container.scrollHeight;
    }

    function updateChatMessage(id, content) {
        var el = document.getElementById(id);
        if (el) {
            el.innerHTML = renderMarkdown(content) + '<span class="streaming-cursor">▌</span>';
            var container = $("chatMsgs");
            if (container) container.scrollTop = container.scrollHeight;
        }
    }

    function finishChat(msgId) {
        _busy = false;
        var sendBtn = $("chatSendBtn");
        if (sendBtn) sendBtn.disabled = false;
        
        var indicator = $("streamingIndicator");
        if (indicator) indicator.style.display = "none";
        
        var el = document.getElementById(msgId);
        if (el) el.innerHTML = renderMarkdown(_streamingText);
        
        _chatHistory.push({ role: "assistant", content: _streamingText });
        if (_chatHistory.length > 20) {
            _chatHistory = _chatHistory.slice(-20);
        }
    }

    /* ===== Markdown 渲染 ===== */
    function renderMarkdown(text) {
        if (!text) return "";
        var html = escapeHtml(text);
        
        // 代码块
        html = html.replace(/```(\w*)\n([\s\S]*?)```/g, function(m, lang, code) {
            return '<pre class="md-code"><code>' + code.trim() + '</code></pre>';
        });
        
        // 行内代码
        html = html.replace(/`([^`]+)`/g, '<code class="md-inline">$1</code>');
        
        // 标题
        html = html.replace(/^### (.+)$/gm, '<div class="md-h" style="font-size:14px">$1</div>');
        html = html.replace(/^## (.+)$/gm, '<div class="md-h" style="font-size:15px">$1</div>');
        html = html.replace(/^# (.+)$/gm, '<div class="md-h" style="font-size:16px">$1</div>');
        
        // 粗体和斜体
        html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
        html = html.replace(/\*([^*]+)\*/g, '<em>$1</em>');
        
        // 列表
        html = html.replace(/^[\-\*] (.+)$/gm, '<li>$1</li>');
        html = html.replace(/(<li>.*<\/li>\n?)+/g, '<ul class="md-list">$&</ul>');
        
        // 换行
        html = html.replace(/\n/g, '<br>');
        
        return html;
    }

    /* ===== 插件初始化 ===== */
    window.Asc.plugin.init = function(text) {
        if (_pluginInitialized) {
            logDebug("Asc.plugin.init() - 已初始化,跳过");
            return;
        }
        _pluginInitialized = true;
        
        logDebug("Asc.plugin.init() 被调用, 文本长度: " + (text ? text.length : 0));
        _fullText = text || "";
        
        // 确保 DOM 完全加载后再初始化
        function initPlugin() {
            logDebug("initPlugin() 被调用");
            
            // 等待 DOM 元素就绪
            var checkInterval = setInterval(function() {
                var btnGrid = document.getElementById("btnGrid");
                var selInfo = document.getElementById("selInfo");
                
                if (btnGrid && selInfo) {
                    clearInterval(checkInterval);
                    logDebug("initPlugin() - DOM elements ready, initializing...");
                    
                    initTabs();
                    renderButtons();

                    // 初始化时获取初始选中文本状态
                    getSelectedText(function(selText) {
                        logDebug("初始化时获取到选中文本长度: " + selText.length);
                        setSelInfo(selText);
                    });
                    
                    getFullText(function() {
                        logDebug("getFullText 回调完成");
                        updateChatStatus();
                    });
                }
            }, 50);  // 每50ms检查一次
        }
        
        // 检查 DOM 是否就绪
        if (document.readyState === 'loading') {
            logDebug("init - DOM 正在加载,等待 DOMContentLoaded...");
            document.addEventListener('DOMContentLoaded', initPlugin);
        } else {
            logDebug("init - DOM 已就绪,准备初始化插件...");
            initPlugin();
        }

        Asc.plugin.attachEvent("onSelectionChanged", function() {
            logDebug("onSelectionChanged 事件触发");
            getSelectedText(function(selText) {
                logDebug("getSelectedText 回调, 长度: " + selText.length);
                setSelInfo(selText);
            });
        });

        // 监听 internalcommand 事件,接收从外部通过 serviceCommand 发送的数据
        if (window.parent && window.parent.Common && window.parent.Common.Gateway) {
            window.parent.Common.Gateway.on('internalcommand', function(data) {
                console.log("[AI插件] 收到 internalcommand 消息:", data);
                
                // 新格式:接收完整配置对象
                if (data.command === 'setPluginConfig') {
                    var config = data.data;
                    if (config && typeof config === 'object') {
                        // 设置 API_BASE
                        if (config.apiBaseUrl && typeof config.apiBaseUrl === 'string' && config.apiBaseUrl.trim()) {
                            API_BASE = config.apiBaseUrl.replace(/\/$/, '') + "/api/v2/";
                            console.log("[AI插件] 通过 internalcommand 设置 API_BASE:", API_BASE);
                        }
                        // 设置 access_token(存储到本地供后续请求使用)
                        if (config.accessToken && typeof config.accessToken === 'string' && config.accessToken.trim()) {
                            try {
                                localStorage.setItem('access_token', config.accessToken);
                                console.log("[AI插件] access_token 存储成功");
                            } catch(e) {
                                console.warn("[AI插件] access_token 存储失败:", e);
                            }
                        }
                        // 设置用户信息
                        if (config.userInfo && typeof config.userInfo === 'string' && config.userInfo.trim()) {
                            try {
                                localStorage.setItem('docworkbench_user', config.userInfo);
                                // 更新 USER 变量
                                try {
                                    var userData = JSON.parse(config.userInfo);
                                    USER = userData.username;
                                } catch(e) {
                                    USER = null;
                                }
                                console.log("[AI插件] 用户信息已存储, 用户名:", USER);
                            } catch(e) {
                                console.warn("[AI插件] 用户信息存储失败:", e);
                            }
                        }
                    }
                }
                
                // 兼容旧格式
                if (data.command === 'setApiBase') {
                    if (data.data && typeof data.data === 'string' && data.data.trim()) {
                        API_BASE = data.data.replace(/\/$/, '') + "/api/v2/";
                        console.log("[AI插件] 通过 setApiBase 设置 API_BASE:", API_BASE);
                    }
                }
            });
        } else {
            console.warn("[AI插件] Common.Gateway 不可用,跳过事件监听器");
        }

        // 发送插件初始化完成消息给编辑器
        if (window.parent && window.parent.Common && window.parent.Common.Gateway) {
            window.parent.Common.Gateway.sendInfo({
                command: 'pluginInitialized',
                data: { status: 200, message: 'AI Plugin initialized', guid: 'asc.{ab64f7db-475f-4318-bb46-a86d24b6a9d2}' }
            });
            console.log("[AI插件] 已发送 pluginInitialized 消息");
        }
    };

    window.Asc.plugin.button = function(id) {
        if (id === -1 || id === 0) {
            this.executeCommand("close", "");
        }
    };

    // 定期刷新全文内容
    setInterval(function() {
        if (!_busy) {
            getFullText(function() {
                updateChatStatus();
            });
        }
    }, 5000);

})(window);
相关推荐
Lucky_Turtle13 小时前
【Vue】element plus Slider小数组件设置顺滑程度
前端·javascript·vue.js
Bigger13 小时前
🔥 一份 Agent 工程岗 JD,暴露了市场真正想要什么样的人
前端·agent·全栈
我头上有犄角ovo13 小时前
我在微信小程序里手搓人脸识别引导,结果被“右转头”和“手遮脸”教育了
前端
David_Xia13 小时前
干爆 11s 提交卡顿!引入 Rust 级 oxlint 彻底拯救团队 Git Commit 噩梦的重构实践
前端
前端环境观察室13 小时前
别急着让 Agent 跑任务,先把浏览器环境上下文建模
前端
Dxy123931021613 小时前
js中Math.min.apply()详解
开发语言·javascript
蝎子莱莱爱打怪13 小时前
零基础用AI写App?兄弟😂 醒醒吧,那只是个玩具罢了!
前端·人工智能·后端
用户13060956072314 小时前
elpis里程碑一的阶段性总结
前端
砍材农夫14 小时前
物联网 基于netty控制报文结构(发布与接收)
java·开发语言·前端·javascript·物联网