diff --git a/background/background.js b/background/background.js index fb1bba8..a5ed3f9 100644 --- a/background/background.js +++ b/background/background.js @@ -1,23 +1,20 @@ -// background.js +// background/background.js -// 监听来自 main.js 的消息 -chrome.runtime.onMessageExternal.addListener((message, sender, sendResponse) => { - // 这种方式需要知道 Extension ID,更简单的方法是统一由 content.js 转发 -}); - -// 通用监听(推荐) +// 监听来自 content.js 的消息转发 chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { if (request.type === "AI_TRANSLATE") { - // 1. 先从 storage 获取配置 - chrome.storage.sync.get(['aiConfig'], async (result) => { + // 1. 同时获取 AI 配置信息和语音开关状态 + chrome.storage.sync.get(['aiConfig', 'voiceEnabled'], async (result) => { const config = result.aiConfig; + const voiceEnabled = result.voiceEnabled || false; // 默认为关闭 + if (!config || !config.apiKey) { - sendResponse({ success: false, error: "未配置 API Key" }); + sendResponse({ success: false, error: "请先在扩展图标中配置 API Key" }); return; } try { - // 2. 发起请求 + // 2. 向 AI 平台发起 fetch 请求 const response = await fetch(`${config.apiUrl}/chat/completions`, { method: 'POST', headers: { @@ -34,12 +31,27 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { }) }); + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error?.message || "网络请求失败"); + } + const data = await response.json(); - sendResponse({ success: true, data: data }); + + // 3. 将 AI 结果和语音开关状态一并返回给 content.js + sendResponse({ + success: true, + data: data, + voiceEnabled: voiceEnabled + }); + } catch (err) { + console.error("AI Request Error:", err); sendResponse({ success: false, error: err.message }); } }); - return true; // 保持异步 + + // 返回 true 表示我们将异步发送响应 + return true; } }); \ No newline at end of file diff --git a/main.js b/main.js index 755175d..95893e7 100644 --- a/main.js +++ b/main.js @@ -20,15 +20,21 @@ const init = async () => { } }; + /** + * 处理 AI 流程的主函数 + */ async function startProcess(text) { if (!text) return; ui.setLoading(true); try { - // 无论是否本地匹配,统一走 translateToCommand 以获取对话回复 - // 如果你希望本地极速响应,可以在此保留逻辑,但建议统一走 AI 获取 - const aiResult = await translateToCommand(text); - if (aiResult) { - handleCommand(aiResult, uiRefs); + // 获取 AI 响应结果(包含内容和语音开关状态) + const aiResponse = await translateToCommand(text); + + if (aiResponse && aiResponse.content) { + // 将内容、UI 引用以及语音开关状态传给指令处理器 + handleCommand(aiResponse.content, uiRefs, aiResponse.voiceEnabled); + } else { + uiRefs.updateStatus("未识别到有效指令"); } } catch (err) { console.error("处理流程错误:", err); @@ -38,28 +44,45 @@ const init = async () => { } } - const voiceCtrl = initVoice(document.getElementById("automation-ai-panel"), (text) => { + // 初始化语音识别模块 + const voiceCtrl = initVoice(ui, (text) => { + ui.setRecording(false); + isRecording = false; ui.input.value = text; startProcess(text); }); - // 按钮触发录音 + // 输入框回车触发 + ui.input.addEventListener("keydown", (e) => { + if (e.key === "Enter") { + const text = ui.input.value.trim(); + ui.input.value = ""; + startProcess(text); + } + }); + + // 按钮点击触发录音 ui.btn.onclick = () => { if (!isRecording) { voiceCtrl.start(); ui.setRecording(true); isRecording = true; + // 4秒自动停止录音保护 setTimeout(() => { if (isRecording) { voiceCtrl.stop(); ui.setRecording(false); isRecording = false; } - }, 4000); // 按钮模式延长到 4s,确保说话完整 + }, 4000); + } else { + voiceCtrl.stop(); + ui.setRecording(false); + isRecording = false; } }; - // 空格长按触发录音 + // 空格长按触发录音逻辑 window.addEventListener("keydown", (e) => { if (e.code === "Space" && e.target.tagName !== "INPUT" && e.target.tagName !== "TEXTAREA") { if (spaceTimer || isRecording) return; @@ -70,7 +93,7 @@ const init = async () => { ui.setRecording(true); isRecording = true; } - }, 500); + }, 500); // 判定为长按 } }); @@ -81,25 +104,20 @@ const init = async () => { spaceTimer = null; } if (isRecording) { + // 延迟停止以捕捉最后一段语音 setTimeout(() => { voiceCtrl.stop(); ui.setRecording(false); isRecording = false; - }, 200); - e.preventDefault(); + }, 300); } } }); - - // 输入框回车触发 - ui.input.onkeydown = (e) => { - if (e.key === "Enter") { - const val = ui.input.value; - ui.input.value = ""; - startProcess(val); - } - }; }; -if (document.readyState === "complete") init(); -else window.addEventListener("load", init); \ No newline at end of file +// 启动初始化 +if (document.readyState === "complete" || document.readyState === "interactive") { + init(); +} else { + window.addEventListener("DOMContentLoaded", init); +} \ No newline at end of file diff --git a/popup/popup.html b/popup/popup.html index f44c0b3..6c7960d 100644 --- a/popup/popup.html +++ b/popup/popup.html @@ -13,6 +13,20 @@ .btn:hover { background: #66b1ff; } #status { font-size: 12px; text-align: center; margin-top: 8px; height: 14px; } .note { font-size: 11px; color: #999; margin-top: 4px; } + + /* 语音开关专属样式 */ + .switch-item { + display: flex; + align-items: center; + justify-content: space-between; + background: #fff; + padding: 10px; + border-radius: 4px; + border: 1px solid #dcdfe6; + margin-top: 15px; + } + .switch-item label { margin-bottom: 0; cursor: pointer; flex: 1; } + .switch-item input { width: auto; cursor: pointer; } @@ -21,7 +35,6 @@
-
通常使用兼容模式地址
@@ -31,12 +44,20 @@
- + +
+ +
+ +
+
+
提示:保存配置后,请刷新目标页面使设置生效。
+ \ No newline at end of file diff --git a/popup/popup.js b/popup/popup.js index 9cc26e2..b315933 100644 --- a/popup/popup.js +++ b/popup/popup.js @@ -1,12 +1,16 @@ // 页面加载时读取存储的配置 document.addEventListener('DOMContentLoaded', () => { - chrome.storage.sync.get(['aiConfig'], (result) => { + // 同时获取 aiConfig 和 voiceEnabled 状态 + chrome.storage.sync.get(['aiConfig', 'voiceEnabled'], (result) => { const config = result.aiConfig || {}; - // 如果没有存储的值,则显示默认占位符或默认值 + // 填充 API 配置 document.getElementById('apiUrl').value = config.apiUrl || DEFAULT_URL; document.getElementById('apiKey').value = config.apiKey || ""; document.getElementById('modelName').value = config.modelName || DEFAULT_MODEL; + + // 填充语音开关状态(默认为关闭 false) + document.getElementById('voiceEnabled').checked = result.voiceEnabled || false; }); }); @@ -18,14 +22,19 @@ document.getElementById('save').addEventListener('click', () => { modelName: document.getElementById('modelName').value.trim() || DEFAULT_MODEL }; + const isVoiceEnabled = document.getElementById('voiceEnabled').checked; + // 验证 API Key 是否填写 if (!config.apiKey) { showStatus("❌ 请输入 API Key", "#f56c6c"); return; } - // 存储到 chrome.storage - chrome.storage.sync.set({ aiConfig: config }, () => { + // 存储到 chrome.storage.sync + chrome.storage.sync.set({ + aiConfig: config, + voiceEnabled: isVoiceEnabled + }, () => { showStatus("✅ 配置已保存,刷新页面生效", "#67c23a"); }); }); @@ -35,6 +44,8 @@ function showStatus(text, color) { const status = document.getElementById('status'); status.textContent = text; status.style.color = color; + + // 3秒后清除提示 setTimeout(() => { status.textContent = ''; }, 3000); diff --git a/scripts/ai.js b/scripts/ai.js index 5d5fad7..c4da5d8 100644 --- a/scripts/ai.js +++ b/scripts/ai.js @@ -26,7 +26,12 @@ export async function translateToCommand(userInput) { if (response.data.choices && response.data.choices.length > 0) { const content = response.data.choices[0].message.content.trim(); console.log("📥 AI 原始响应:", content); - resolve(content === "UNKNOWN" ? null : content); + + // 返回包含内容和语音开关状态的对象 + resolve({ + content: content === "UNKNOWN" ? null : content, + voiceEnabled: response.voiceEnabled // 从 background.js 透传回来的开关状态 + }); } else { resolve(null); } @@ -35,11 +40,15 @@ export async function translateToCommand(userInput) { resolve(null); } } else { + console.error("AI 请求失败:", response?.error); resolve(null); } }; + // 监听来自 content.js 的结果反馈 window.addEventListener("AI_RESULT", handler); + + // 触发自定义事件,由 content.js 转发给 background.js window.dispatchEvent(new CustomEvent("DO_AI_REQUEST", { detail: { userInput, systemPrompt } })); diff --git a/scripts/commands.js b/scripts/commands.js index 5a48f9c..d677e77 100644 --- a/scripts/commands.js +++ b/scripts/commands.js @@ -1,4 +1,6 @@ // scripts/commands.js + +// 标准指令路由映射表 export const COMMANDS = [ { key: "首页", menu: "首页", route: "/首页" }, { key: "添加设备", menu: "添加设备", route: "/添加设备/添加设备" }, @@ -14,87 +16,69 @@ export const COMMANDS = [ { key: "报警通知", menu: "报警通知", route: "/报警管理/报警通知" }, { key: "报警记录", menu: "报警记录", route: "/报警管理/报警记录" }, { key: "基础报表", menu: "基础报表", route: "/报表管理/基础报表" }, - { key: "高级报表配置", menu: "高级报表配置", route: "/报表管理/高级报表配置" }, - { key: "高级报表", menu: "高级报表", route: "/报表管理/高级报表" }, - { key: "应用场景", menu: "应用场景", route: "/仪表管理/应用场景" }, - { key: "仪表管理", menu: "仪表管理", route: "/仪表管理/仪表管理" }, - { key: "虚拟仪表", menu: "虚拟仪表", route: "/仪表管理/虚拟仪表" }, - { key: "物位监测", menu: "物位监测", route: "/场景管理/物位监测/物位监测" }, - { key: "物位监测配置", menu: "物位监测配置", route: "/场景管理/物位监测/物位监测配置" }, - { key: "车间看板", menu: "车间看板", route: "/场景管理/车间看板/车间看板" }, - { key: "车间看板配置", menu: "车间看板配置", route: "/场景管理/车间看板/车间看板配置" }, - { key: "能源结算", menu: "能源结算", route: "/场景管理/能源抄表/能源结算" }, - { key: "能源结算配置", menu: "能源结算配置", route: "/场景管理/能源抄表/能源结算配置" }, - { key: "多租户能源结算", menu: "多租户能源结算", route: "/场景管理/多租户结算/多租户能源结算" }, - { key: "租户管理", menu: "租户管理", route: "/场景管理/多租户结算/租户管理" }, - { key: "计价方式管理", menu: "计价方式管理", route: "/场景管理/多租户结算/计价方式管理" }, - { key: "单染缸印染结算", menu: "单染缸印染结算", route: "/场景管理/印染结算/单染缸印染结算" }, - { key: "多染缸印染结算", menu: "多染缸印染结算", route: "/场景管理/印染结算/多染缸印染结算" }, - { key: "染缸能耗一览表", menu: "染缸能耗一览表", route: "/场景管理/印染结算/染缸能耗一览表" }, - { key: "印染结算配置", menu: "印染结算配置", route: "/场景管理/印染结算/印染结算配置" }, - { key: "尘埃粒子车间", menu: "尘埃粒子车间", route: "/场景管理/尘埃粒子/尘埃粒子车间" }, - { key: "洁净度一览表", menu: "洁净度一览表", route: "/场景管理/尘埃粒子/洁净度一览表" }, - { key: "尘埃粒子配置", menu: "尘埃粒子配置", route: "/场景管理/尘埃粒子/尘埃粒子配置" }, - { key: "消息管理", menu: "消息管理", route: "/系统管理/消息管理" }, - { key: "数据服务", menu: "数据服务", route: "/系统管理/数据服务" }, - { key: "数据下云", menu: "数据下云", route: "/系统管理/数据下云" }, + { key: "分析报表", menu: "分析报表", route: "/报表管理/分析报表" }, + { key: "区域管理", menu: "区域管理", route: "/系统管理/区域管理" }, + { key: "角色管理", menu: "角色管理", route: "/系统管理/角色管理" }, + { key: "用户管理", menu: "用户管理", route: "/系统管理/用户管理" } ]; -function waitForElement(selector, callback, timeout = 5000) { - const start = Date.now(); - const timer = setInterval(() => { - const el = document.querySelector(selector); - if (el) { - clearInterval(timer); - callback(el); - } else if (Date.now() - start > timeout) { - clearInterval(timer); - console.warn("等待元素超时:", selector); +/** + * 展开侧边栏父级菜单 + */ +function expandParentMenu(span) { + let parent = span.closest('li'); + while (parent) { + if (parent.classList.contains('el-submenu')) { + const title = parent.querySelector('.el-submenu__title'); + if (title && !parent.classList.contains('is-opened')) { + title.click(); + } } - }, 200); -} - -function autoFillAndSubmit(value) { - waitForElement('.el-input__inner', (input) => { - input.value = value; - input.dispatchEvent(new Event('input', { bubbles: true })); - input.dispatchEvent(new Event('change', { bubbles: true })); - - const submitBtn = document.querySelector('.el-button--primary'); - if (submitBtn) { - setTimeout(() => { - submitBtn.click(); - }, 600); - } - }); -} - -export function expandParentMenu(span) { - const subMenu = span.closest(".el-sub-menu"); - if (subMenu && !subMenu.classList.contains("is-opened")) { - const title = subMenu.querySelector(".el-sub-menu__title"); - if (title) title.click(); + parent = parent.parentElement.closest('li'); } } /** - * 处理 AI 返回的混合指令 (对话 + 操作) + * 自动填充并提交搜索(模拟针对某些页面的操作) */ -export function handleCommand(aiResult, uiRefs) { +function autoFillAndSubmit(arg) { + if (!arg) return; + setTimeout(() => { + const input = document.querySelector('input[placeholder*="名称"], input[placeholder*="编号"]'); + if (input) { + input.value = arg; + input.dispatchEvent(new Event('input', { bubbles: true })); + const searchBtn = document.querySelector('button.el-button--primary'); + if (searchBtn) searchBtn.click(); + } + }, 1000); +} + +/** + * 核心处理器:根据 AI 结果执行动作 + * @param {string} aiResult AI 返回的 XML 字符串 + * @param {object} uiRefs UI 更新引用的对象 + * @param {boolean} voiceEnabled 是否允许播放语音 (由 ai.js 传入) + */ +export function handleCommand(aiResult, uiRefs, voiceEnabled = false) { if (!aiResult || aiResult === "UNKNOWN") return; // 1. 解析对话内容并反馈 const commMatch = aiResult.match(/([\s\S]*?)<\/communication>/); if (commMatch && commMatch[1]) { const speechText = commMatch[1].trim(); - // 更新 UI 状态 + + // 始终更新 UI 界面上的文字状态 if (uiRefs && uiRefs.updateStatus) { uiRefs.updateStatus(speechText); } - // 语音播报 - const utterance = new SpeechSynthesisUtterance(speechText); - utterance.lang = "zh-CN"; - window.speechSynthesis.speak(utterance); + + // 仅在语音开关开启时播报语音 + if (voiceEnabled) { + const utterance = new SpeechSynthesisUtterance(speechText); + utterance.lang = "zh-CN"; + window.speechSynthesis.speak(utterance); + } } // 2. 解析指令逻辑 @@ -105,22 +89,31 @@ export function handleCommand(aiResult, uiRefs) { const arg = argMatch ? argMatch[1].trim() : null; if (key) { + // 模糊匹配指令 const command = COMMANDS.find(c => c.key === key) || COMMANDS.find(c => key.includes(c.key)); if (command) { + console.log("🚀 执行指令:", command.key, "参数:", arg); + + // 尝试点击侧边栏菜单(针对 Element UI 结构) const allSpans = Array.from(document.querySelectorAll("span")); let span = allSpans.find(el => el.innerText.trim() === command.menu); if (span) { expandParentMenu(span); span.click(); - window.location.hash = command.route; - if (arg) autoFillAndSubmit(arg); - } else { - window.location.hash = command.route; - if (arg) autoFillAndSubmit(arg); } + + // 路由跳转 + window.location.hash = command.route; + + // 自动填充搜索参数 + if (arg) { + autoFillAndSubmit(arg); + } + } else { + console.warn("⚠️ 未找到匹配指令:", key); } } } \ No newline at end of file