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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
}
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);