Compare commits
10
Commits
432bc69120
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a42aa571d1 | ||
|
|
24e8f87171 | ||
|
|
b2e1860f67 | ||
|
|
34d658c2b9 | ||
|
|
1b8f833789 | ||
|
|
c43b5e5ac1 | ||
|
|
eaf75ac17f | ||
|
|
53994476c4 | ||
|
|
e806d61ede | ||
|
|
9e486fa390 |
No files matched your search
@@ -1,3 +1,45 @@
|
|||||||
# Agent_For_Supmea
|
# Agent_For_Supmea
|
||||||
|
|
||||||
本项目是针对隶属于杭州美仪自动化股份有限公司的仪表云Web平台的一款浏览器插件。此插件支持通过语音/文字输入实现对界面的操作,并且支持接入大语言模型的API。
|
本项目是针对隶属于杭州美仪自动化股份有限公司的仪表云Web平台的一款浏览器插件。此插件支持通过语音/文字输入实现对界面的操作,并且支持接入大语言模型的API。
|
||||||
|
|
||||||
|
## 实现原理
|
||||||
|
|
||||||
|
本项目通过LLM模型对接收到的自然语言进行处理,提取出其中的关键词用于控制界面的跳转、文本框输入以及按钮的点击功能。
|
||||||
|
|
||||||
|
LLM模型就像一个只有脑袋没有四肢的人,他有天马行空的想法,但他没有双手双脚去实现。
|
||||||
|
|
||||||
|
关键词控制就是一个完全听取执行命令没有判断能力的人,叫他做什么他都可以按步骤执行,但他没有自己的思考。
|
||||||
|
|
||||||
|
将二者结合,将其优势进行互补,就可以实现类似有自主分析决断、自主操作控制的AI Agent。
|
||||||
|
|
||||||
|
## 使用操作
|
||||||
|
|
||||||
|
- 下载文件压缩包并将其解压。
|
||||||
|
|
||||||
|
- 打开浏览器(chrome、edge)在地址栏输入下列地址,,进入到拓展界面。
|
||||||
|
|
||||||
|
```c
|
||||||
|
edge://extensions/
|
||||||
|
```
|
||||||
|
|
||||||
|
- 打开开发人员模式。
|
||||||
|
|
||||||
|
- 点击解压缩的拓展,并选择刚刚解压的文件夹。
|
||||||
|
|
||||||
|
- 访问仪表云: https://1718cloud.com
|
||||||
|
|
||||||
|
- 在浏览器右上角点击拓展图标中的Agent_For_Supmea插件,输入LLM模型的API、Key、Model后点击确认完成操作。
|
||||||
|
|
||||||
|
> **注**:目前测试API支持**硅基流动**、**魔搭(modelscape)**、**OpenRouter**。
|
||||||
|
|
||||||
|
## 感谢
|
||||||
|
|
||||||
|
此项目作为最后的礼物赠与杭州美仪数字化业务部,感谢数字化业务部对我的认可与信任。
|
||||||
|
|
||||||
|
这仅代表一种设计思路,不代表是最好的方案,但肯定的是最好的方案一定会在未来的探索中被发现!
|
||||||
|
|
||||||
|
## 项目地址
|
||||||
|
|
||||||
|
项目地址:https://gitea.dreamlife.top/Cx330/Agent_For_Supmea/
|
||||||
|
|
||||||
|
作者:张梦南
|
||||||
+23
-14
@@ -1,23 +1,18 @@
|
|||||||
// background.js
|
// 监听来自 content.js 的消息转发
|
||||||
|
|
||||||
// 监听来自 main.js 的消息
|
|
||||||
chrome.runtime.onMessageExternal.addListener((message, sender, sendResponse) => {
|
|
||||||
// 这种方式需要知道 Extension ID,更简单的方法是统一由 content.js 转发
|
|
||||||
});
|
|
||||||
|
|
||||||
// 通用监听(推荐)
|
|
||||||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||||
if (request.type === "AI_TRANSLATE") {
|
if (request.type === "AI_TRANSLATE") {
|
||||||
// 1. 先从 storage 获取配置
|
// 获取 AI 配置信息和语音开关状态
|
||||||
chrome.storage.sync.get(['aiConfig'], async (result) => {
|
chrome.storage.sync.get(['aiConfig', 'voiceEnabled'], async (result) => {
|
||||||
const config = result.aiConfig;
|
const config = result.aiConfig;
|
||||||
|
const voiceEnabled = result.voiceEnabled || false; // 默认为关闭
|
||||||
|
|
||||||
if (!config || !config.apiKey) {
|
if (!config || !config.apiKey) {
|
||||||
sendResponse({ success: false, error: "未配置 API Key" });
|
sendResponse({ success: false, error: "请先在扩展图标中配置 API Key" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 2. 发起请求
|
// 向 AI 平台发起 fetch 请求
|
||||||
const response = await fetch(`${config.apiUrl}/chat/completions`, {
|
const response = await fetch(`${config.apiUrl}/chat/completions`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -34,12 +29,26 @@ 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();
|
const data = await response.json();
|
||||||
sendResponse({ success: true, data: data });
|
|
||||||
|
// 将 AI 结果和语音开关状态返回给 content.js
|
||||||
|
sendResponse({
|
||||||
|
success: true,
|
||||||
|
data: data,
|
||||||
|
voiceEnabled: voiceEnabled
|
||||||
|
});
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error("AI Request Error:", err);
|
||||||
sendResponse({ success: false, error: err.message });
|
sendResponse({ success: false, error: err.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return true; // 保持异步
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
+2
-2
@@ -3,7 +3,7 @@ script.type = 'module';
|
|||||||
script.src = chrome.runtime.getURL('main.js');
|
script.src = chrome.runtime.getURL('main.js');
|
||||||
document.head.appendChild(script);
|
document.head.appendChild(script);
|
||||||
|
|
||||||
// 2. 监听来自网页(main.js)的请求
|
// 监听来自 main.js 的请求
|
||||||
window.addEventListener("DO_AI_REQUEST", async (event) => {
|
window.addEventListener("DO_AI_REQUEST", async (event) => {
|
||||||
const { userInput, systemPrompt } = event.detail;
|
const { userInput, systemPrompt } = event.detail;
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ window.addEventListener("DO_AI_REQUEST", async (event) => {
|
|||||||
userInput,
|
userInput,
|
||||||
systemPrompt
|
systemPrompt
|
||||||
}, (response) => {
|
}, (response) => {
|
||||||
// 将结果传回给网页(main.js)
|
// 将结果传回给 main.js
|
||||||
window.dispatchEvent(new CustomEvent("AI_RESULT", { detail: response }));
|
window.dispatchEvent(new CustomEvent("AI_RESULT", { detail: response }));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -3,60 +3,109 @@ import { handleCommand, COMMANDS } from './scripts/commands.js';
|
|||||||
import { initVoice } from './scripts/voice.js';
|
import { initVoice } from './scripts/voice.js';
|
||||||
import { translateToCommand } from './scripts/ai.js';
|
import { translateToCommand } from './scripts/ai.js';
|
||||||
|
|
||||||
// 等待页面加载完成
|
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
console.log("🚀 插件主程序启动...");
|
|
||||||
const ui = createPanel();
|
const ui = createPanel();
|
||||||
|
let spaceTimer = null;
|
||||||
|
let isRecording = false;
|
||||||
|
|
||||||
|
// 统一定义 UI 更新引用
|
||||||
|
const uiRefs = {
|
||||||
|
updateStatus: (text) => {
|
||||||
|
const statusText = document.getElementById("statusText");
|
||||||
|
if (statusText) {
|
||||||
|
statusText.innerText = text;
|
||||||
|
statusText.style.color = "#409eff";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
async function startProcess(text) {
|
async function startProcess(text) {
|
||||||
const rawText = text.trim();
|
if (!text) return;
|
||||||
if (!rawText) return;
|
|
||||||
|
|
||||||
console.log("📩 接收到输入:", rawText);
|
|
||||||
|
|
||||||
// 1. 本地匹配逻辑
|
|
||||||
const localMatch = COMMANDS.find(c => rawText.includes(c.key));
|
|
||||||
if (localMatch) {
|
|
||||||
console.log("🎯 本地关键词匹配成功:", localMatch.key);
|
|
||||||
handleCommand(localMatch.key);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. AI 翻译逻辑
|
|
||||||
ui.setLoading(true);
|
ui.setLoading(true);
|
||||||
try {
|
try {
|
||||||
const aiResult = await translateToCommand(rawText);
|
const aiResponse = await translateToCommand(text);
|
||||||
if (aiResult) {
|
|
||||||
handleCommand(aiResult);
|
if (aiResponse && aiResponse.content) {
|
||||||
|
handleCommand(aiResponse.content, uiRefs, aiResponse.voiceEnabled);
|
||||||
} else {
|
} else {
|
||||||
console.warn("❌ AI 无法理解该指令");
|
uiRefs.updateStatus("未识别到有效指令");
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("AI 流程出错:", err);
|
console.error("处理流程错误:", err);
|
||||||
|
uiRefs.updateStatus("处理指令时出错");
|
||||||
} finally {
|
} finally {
|
||||||
ui.setLoading(false);
|
ui.setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 输入框回车事件监听
|
// 初始化语音识别模块
|
||||||
|
const voiceCtrl = initVoice(ui, (text) => {
|
||||||
|
ui.setRecording(false);
|
||||||
|
isRecording = false;
|
||||||
|
ui.input.value = text;
|
||||||
|
startProcess(text);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 输入框回车触发
|
||||||
ui.input.addEventListener("keydown", (e) => {
|
ui.input.addEventListener("keydown", (e) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
e.preventDefault(); // 防止某些页面表单提交刷新
|
const text = ui.input.value.trim();
|
||||||
const val = ui.input.value;
|
|
||||||
ui.input.value = "";
|
ui.input.value = "";
|
||||||
startProcess(val);
|
startProcess(text);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 语音识别初始化
|
// 按钮点击触发录音
|
||||||
// 修正:确保 voice.js 里的 handleCommand 回调指向我们的 startProcess
|
ui.btn.onclick = () => {
|
||||||
initVoice(document.getElementById("automation-ai-panel"), (spokenText) => {
|
if (!isRecording) {
|
||||||
ui.input.value = spokenText;
|
voiceCtrl.start();
|
||||||
startProcess(spokenText);
|
ui.setRecording(true);
|
||||||
|
isRecording = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
if (isRecording) {
|
||||||
|
voiceCtrl.stop();
|
||||||
|
ui.setRecording(false);
|
||||||
|
isRecording = false;
|
||||||
|
}
|
||||||
|
}, 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;
|
||||||
|
spaceTimer = setTimeout(() => {
|
||||||
|
if (voiceCtrl.supportSpeech) {
|
||||||
|
voiceCtrl.start();
|
||||||
|
ui.setRecording(true);
|
||||||
|
isRecording = true;
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("keyup", (e) => {
|
||||||
|
if (e.code === "Space") {
|
||||||
|
if (spaceTimer) {
|
||||||
|
clearTimeout(spaceTimer);
|
||||||
|
spaceTimer = null;
|
||||||
|
}
|
||||||
|
if (isRecording) {
|
||||||
|
setTimeout(() => {
|
||||||
|
voiceCtrl.stop();
|
||||||
|
ui.setRecording(false);
|
||||||
|
isRecording = false;
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 确保在 DOM 完成后运行
|
|
||||||
if (document.readyState === "complete" || document.readyState === "interactive") {
|
if (document.readyState === "complete" || document.readyState === "interactive") {
|
||||||
init();
|
init();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "Supmea Automation AI",
|
"name": "Agent_For_Supmea",
|
||||||
"version": "0.1.0",
|
"version": "0.1.17",
|
||||||
"permissions": ["activeTab", "storage"],
|
"permissions": ["activeTab", "storage"],
|
||||||
"background": {
|
"background": {
|
||||||
"service_worker": "background/background.js"
|
"service_worker": "background/background.js"
|
||||||
|
|||||||
+24
-3
@@ -13,15 +13,28 @@
|
|||||||
.btn:hover { background: #66b1ff; }
|
.btn:hover { background: #66b1ff; }
|
||||||
#status { font-size: 12px; text-align: center; margin-top: 8px; height: 14px; }
|
#status { font-size: 12px; text-align: center; margin-top: 8px; height: 14px; }
|
||||||
.note { font-size: 11px; color: #999; margin-top: 4px; }
|
.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; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h3>⚙️ AI 模型配置</h3>
|
<h3> AI 模型配置</h3>
|
||||||
|
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<label>API Base URL</label>
|
<label>API Base URL</label>
|
||||||
<input type="text" id="apiUrl" placeholder="https://api-inference.modelscope.cn/v1">
|
<input type="text" id="apiUrl" placeholder="https://api-inference.modelscope.cn/v1">
|
||||||
<div class="note">通常使用兼容模式地址</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="item">
|
<div class="item">
|
||||||
@@ -31,12 +44,20 @@
|
|||||||
|
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<label>Model Name</label>
|
<label>Model Name</label>
|
||||||
<input type="text" id="modelName" placeholder="deepseek-ai/DeepSeek-V3.2">
|
<input type="text" id="modelName" placeholder="deepseek-ai/DeepSeek-V3">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item switch-item">
|
||||||
|
<label for="voiceEnabled">开启语音回复</label>
|
||||||
|
<input type="checkbox" id="voiceEnabled">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button id="save" class="btn">保存并生效</button>
|
<button id="save" class="btn">保存并生效</button>
|
||||||
|
|
||||||
<div id="status"></div>
|
<div id="status"></div>
|
||||||
|
|
||||||
|
<div class="note">提示:保存配置后,请刷新目标页面使设置生效。</div>
|
||||||
|
|
||||||
<script src="popup.js"></script>
|
<script src="popup.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+15
-6
@@ -1,12 +1,15 @@
|
|||||||
// 页面加载时读取存储的配置
|
// 页面加载时读取存储的配置
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
chrome.storage.sync.get(['aiConfig'], (result) => {
|
|
||||||
|
chrome.storage.sync.get(['aiConfig', 'voiceEnabled'], (result) => {
|
||||||
const config = result.aiConfig || {};
|
const config = result.aiConfig || {};
|
||||||
|
|
||||||
// 如果没有存储的值,则显示默认占位符或默认值
|
|
||||||
document.getElementById('apiUrl').value = config.apiUrl || DEFAULT_URL;
|
document.getElementById('apiUrl').value = config.apiUrl || DEFAULT_URL;
|
||||||
document.getElementById('apiKey').value = config.apiKey || "";
|
document.getElementById('apiKey').value = config.apiKey || "";
|
||||||
document.getElementById('modelName').value = config.modelName || DEFAULT_MODEL;
|
document.getElementById('modelName').value = config.modelName || DEFAULT_MODEL;
|
||||||
|
|
||||||
|
// 语音开关状态(默认为关闭)
|
||||||
|
document.getElementById('voiceEnabled').checked = result.voiceEnabled || false;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -18,23 +21,29 @@ document.getElementById('save').addEventListener('click', () => {
|
|||||||
modelName: document.getElementById('modelName').value.trim() || DEFAULT_MODEL
|
modelName: document.getElementById('modelName').value.trim() || DEFAULT_MODEL
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isVoiceEnabled = document.getElementById('voiceEnabled').checked;
|
||||||
|
|
||||||
// 验证 API Key 是否填写
|
// 验证 API Key 是否填写
|
||||||
if (!config.apiKey) {
|
if (!config.apiKey) {
|
||||||
showStatus("❌ 请输入 API Key", "#f56c6c");
|
showStatus("请输入 API Key", "#f56c6c");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 存储到 chrome.storage
|
// 存储到 chrome.storage.sync
|
||||||
chrome.storage.sync.set({ aiConfig: config }, () => {
|
chrome.storage.sync.set({
|
||||||
|
aiConfig: config,
|
||||||
|
voiceEnabled: isVoiceEnabled
|
||||||
|
}, () => {
|
||||||
showStatus("✅ 配置已保存,刷新页面生效", "#67c23a");
|
showStatus("✅ 配置已保存,刷新页面生效", "#67c23a");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 状态提示函数
|
|
||||||
function showStatus(text, color) {
|
function showStatus(text, color) {
|
||||||
const status = document.getElementById('status');
|
const status = document.getElementById('status');
|
||||||
status.textContent = text;
|
status.textContent = text;
|
||||||
status.style.color = color;
|
status.style.color = color;
|
||||||
|
|
||||||
|
// 3秒后清除提示
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
status.textContent = '';
|
status.textContent = '';
|
||||||
}, 3000);
|
}, 3000);
|
||||||
|
|||||||
+34
-10
@@ -1,28 +1,52 @@
|
|||||||
// ai.js
|
|
||||||
import { COMMANDS } from './commands.js';
|
import { COMMANDS } from './commands.js';
|
||||||
|
|
||||||
export async function translateToCommand(userInput) {
|
export async function translateToCommand(userInput) {
|
||||||
const availableKeys = COMMANDS.map(c => c.key).join(', ');
|
const availableKeys = COMMANDS.map(c => c.key).join(', ');
|
||||||
const systemPrompt = `你是一个助手。将输入映射到以下关键词之一:${availableKeys}。只输出关键词,不要其他字。无法匹配则输出 UNKNOWN。`;
|
|
||||||
|
const systemPrompt = `你是美小智,是仪表云平台的自动化小助手。
|
||||||
|
标准指令列表:[${availableKeys}]
|
||||||
|
|
||||||
|
任务规则:
|
||||||
|
1. **必须包含对话**:无论是否执行指令,你都必须在 <communication>XXXX</communication> 标签中放入你对用户说的话。
|
||||||
|
2. **意图识别**:如果用户意图匹配指令列表,输出 <cmd>标准指令</cmd>。如有参数(如设备号),输出 <arg>参数</arg>。
|
||||||
|
3. **示例**:
|
||||||
|
- 用户:“打开监控中心” -> “<communication>好的,正在为您进入监控中心。</communication><cmd>监控中心</cmd>”
|
||||||
|
- 用户:“添加设备 888” -> “<communication>没问题,正在为您添加编号为 888 的设备。</communication><cmd>添加设备</cmd><arg>888</arg>”
|
||||||
|
4. 只输出 XML 结果,不要输出任何解释说明,保持简洁。`;
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
// 1. 监听结果
|
|
||||||
const handler = (event) => {
|
const handler = (event) => {
|
||||||
const response = event.detail;
|
const response = event.detail;
|
||||||
window.removeEventListener("AI_RESULT", handler);
|
window.removeEventListener("AI_RESULT", handler);
|
||||||
|
|
||||||
if (response.success) {
|
if (response && response.success && response.data) {
|
||||||
const content = response.data.choices[0].message.content.trim();
|
try {
|
||||||
console.log("📥 AI 识别结果:", content);
|
if (response.data.choices && response.data.choices.length > 0) {
|
||||||
resolve(content === "UNKNOWN" ? null : content);
|
const content = response.data.choices[0].message.content.trim();
|
||||||
|
console.log("📥 AI 原始响应:", content);
|
||||||
|
|
||||||
|
// 返回包含内容和语音开关状态的对象
|
||||||
|
resolve({
|
||||||
|
content: content === "UNKNOWN" ? null : content,
|
||||||
|
voiceEnabled: response.voiceEnabled // 从 background.js 透传回来的开关状态
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("解析响应失败:", e);
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error("AI 失败:", response.error);
|
console.error("AI 请求失败:", response?.error);
|
||||||
resolve(null);
|
resolve(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 监听来自 content.js 的结果反馈
|
||||||
window.addEventListener("AI_RESULT", handler);
|
window.addEventListener("AI_RESULT", handler);
|
||||||
|
|
||||||
// 2. 触发请求
|
// 触发自定义事件,由 content.js 转发给 background.js
|
||||||
window.dispatchEvent(new CustomEvent("DO_AI_REQUEST", {
|
window.dispatchEvent(new CustomEvent("DO_AI_REQUEST", {
|
||||||
detail: { userInput, systemPrompt }
|
detail: { userInput, systemPrompt }
|
||||||
}));
|
}));
|
||||||
|
|||||||
+88
-39
@@ -39,51 +39,100 @@ export const COMMANDS = [
|
|||||||
{ key: "数据下云", menu: "数据下云", route: "/系统管理/数据下云" },
|
{ key: "数据下云", menu: "数据下云", route: "/系统管理/数据下云" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function expandParentMenu(span) {
|
function expandParentMenu(span) {
|
||||||
const subMenu = span.closest(".el-sub-menu");
|
let parent = span.closest('li');
|
||||||
if (subMenu) {
|
while (parent) {
|
||||||
const title = subMenu.querySelector(".el-sub-menu__title");
|
if (parent.classList.contains('el-submenu')) {
|
||||||
if (title && !subMenu.classList.contains("is-opened")) {
|
const title = parent.querySelector('.el-submenu__title');
|
||||||
title.click();
|
if (title && !parent.classList.contains('is-opened')) {
|
||||||
console.log("📂 已展开父菜单");
|
title.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parent = parent.parentElement.closest('li');
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function goToRoute(route) {
|
function autoFillAndSubmit(arg) {
|
||||||
// 兼容 Hash 路由和普通路由,根据实际项目情况
|
if (!arg) return;
|
||||||
window.location.hash = route;
|
// 等待页面跳转和组件渲染
|
||||||
console.log("✅ 已跳转到路由:", route);
|
setTimeout(() => {
|
||||||
|
// 尝试寻找各种可能的输入框(针对 Element UI)
|
||||||
|
const input = document.querySelector('input[placeholder*="编号"]') ||
|
||||||
|
document.querySelector('input[placeholder*="名称"]') ||
|
||||||
|
document.querySelector('.el-input__inner');
|
||||||
|
|
||||||
|
if (input) {
|
||||||
|
input.value = arg;
|
||||||
|
// 触发 input 事件让 Vue 监听到数据变化
|
||||||
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
|
||||||
|
// 寻找包含“添加”、“查询”
|
||||||
|
setTimeout(() => {
|
||||||
|
const buttons = Array.from(document.querySelectorAll('button'));
|
||||||
|
const submitBtn = buttons.find(btn =>
|
||||||
|
btn.innerText.includes("添加") ||
|
||||||
|
btn.innerText.includes("查询") ||
|
||||||
|
btn.innerText.includes("确认") ||
|
||||||
|
btn.classList.contains('el-button--primary')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (submitBtn) {
|
||||||
|
submitBtn.click();
|
||||||
|
console.log("已自动点击按钮:", submitBtn.innerText);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}, 1200);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleCommand(keyOrText) {
|
export function handleCommand(aiResult, uiRefs, voiceEnabled = false) {
|
||||||
console.log("🚀 执行指令逻辑:", keyOrText);
|
|
||||||
|
|
||||||
// 尝试精确匹配 (AI返回的Key) 或 模糊匹配 (用户输入的Text)
|
|
||||||
const command = COMMANDS.find(c => c.key === keyOrText) ||
|
|
||||||
COMMANDS.find(c => keyOrText.includes(c.key));
|
|
||||||
|
|
||||||
if (!command) {
|
if (typeof aiResult !== 'string' || aiResult === "UNKNOWN") return;
|
||||||
console.warn("无法执行,未找到对应菜单动作:", keyOrText);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查找菜单 DOM 元素 (根据你原有的逻辑)
|
// 解析对话内容
|
||||||
const allSpans = Array.from(document.querySelectorAll("span"));
|
const commMatch = aiResult.match(/<communication>([\s\S]*?)<\/communication>/);
|
||||||
|
if (commMatch && commMatch[1]) {
|
||||||
// 优先全匹配,防止"报表"匹配到"高级报表"
|
const speechText = commMatch[1].trim();
|
||||||
let span = allSpans.find(el => el.innerText.trim() === command.menu);
|
if (uiRefs && uiRefs.updateStatus) {
|
||||||
|
uiRefs.updateStatus(speechText);
|
||||||
|
}
|
||||||
|
|
||||||
if (span) {
|
// 语音播放判断
|
||||||
expandParentMenu(span);
|
if (voiceEnabled) {
|
||||||
// 模拟点击触发 Vue/React 的路由跳转,或者直接改 Hash
|
const utterance = new SpeechSynthesisUtterance(speechText);
|
||||||
// 如果页面需要点击才能触发加载,建议 span.click()
|
utterance.lang = "zh-CN";
|
||||||
// 如果只是纯 hash 跳转,goToRoute 即可
|
window.speechSynthesis.speak(utterance);
|
||||||
|
}
|
||||||
// 建议先点击,确保侧边栏高亮
|
}
|
||||||
span.click();
|
|
||||||
goToRoute(command.route);
|
// 解析指令和参数
|
||||||
} else {
|
const cmdMatch = aiResult.match(/<cmd>([\s\S]*?)<\/cmd>/);
|
||||||
console.error(`❌ 未找到菜单元素: ${command.menu}`);
|
const argMatch = aiResult.match(/<arg>([\s\S]*?)<\/arg>/);
|
||||||
}
|
|
||||||
|
const key = cmdMatch ? cmdMatch[1].trim() : null;
|
||||||
|
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) {
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+35
-21
@@ -1,6 +1,5 @@
|
|||||||
export function createPanel() {
|
export function createPanel() {
|
||||||
const panelId = "automation-ai-panel";
|
const panelId = "automation-ai-panel";
|
||||||
// 防止重复创建
|
|
||||||
let panel = document.getElementById(panelId);
|
let panel = document.getElementById(panelId);
|
||||||
if (panel) return getUIRefs(panel);
|
if (panel) return getUIRefs(panel);
|
||||||
|
|
||||||
@@ -10,21 +9,30 @@ export function createPanel() {
|
|||||||
position: fixed; bottom: 24px; right: 24px; z-index: 2147483647;
|
position: fixed; bottom: 24px; right: 24px; z-index: 2147483647;
|
||||||
background: white; border-radius: 10px;
|
background: white; border-radius: 10px;
|
||||||
box-shadow: 0 4px 12px rgba(0,0,0,.2); padding: 12px; width: 260px;
|
box-shadow: 0 4px 12px rgba(0,0,0,.2); padding: 12px; width: 260px;
|
||||||
font-size: 14px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
font-size: 14px; font-family: sans-serif;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
panel.innerHTML = `
|
panel.innerHTML = `
|
||||||
|
<style>
|
||||||
|
@keyframes breathe {
|
||||||
|
0% { opacity: 1; transform: scale(1); }
|
||||||
|
50% { opacity: 0.5; transform: scale(1.1); }
|
||||||
|
100% { opacity: 1; transform: scale(1); }
|
||||||
|
}
|
||||||
|
.recording-dot {
|
||||||
|
width: 8px; height: 8px; background: #ff4d4f; border-radius: 50%;
|
||||||
|
display: none; margin-right: 6px; animation: breathe 1.2s infinite;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<div style="display:flex;gap:8px;align-items:center;">
|
<div style="display:flex;gap:8px;align-items:center;">
|
||||||
<input id="voiceTextInput" type="text" placeholder="输入指令或自然语言..."
|
<input id="voiceTextInput" type="text" placeholder="输入指令或长按空格..."
|
||||||
style="flex:1;padding:8px;border:1px solid #dcdfe6;border-radius:4px;outline:none;font-size:13px;" />
|
style="flex:1;padding:8px;border:1px solid #dcdfe6;border-radius:4px;outline:none;font-size:13px;" />
|
||||||
<button id="voiceBtn" title="按 Alt+V 快捷开启"
|
<button id="voiceBtn" style="padding:8px 12px;border:none;border-radius:4px;background:#409eff;color:#fff;cursor:pointer;">🎤</button>
|
||||||
style="padding:8px 12px;border:none;border-radius:4px;background:#409eff;color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;">
|
|
||||||
🎤
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="panelStatus" style="margin-top:8px;color:#999;font-size:12px;display:flex;justify-content:space-between;align-items:center;">
|
<div id="panelStatus" style="margin-top:10px;color:#999;font-size:12px;display:flex;align-items:center;">
|
||||||
|
<div id="recordingIndicator" class="recording-dot"></div>
|
||||||
<span id="statusText">准备就绪</span>
|
<span id="statusText">准备就绪</span>
|
||||||
<span id="aiLoading" style="display:none;color:#409eff;font-weight:bold;">🤖 思考中...</span>
|
<span id="aiLoading" style="display:none;color:#409eff;margin-left:auto;font-weight:bold;">🤖 思考中...</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -33,19 +41,25 @@ export function createPanel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getUIRefs(panel) {
|
function getUIRefs(panel) {
|
||||||
const input = panel.querySelector("#voiceTextInput");
|
|
||||||
const btn = panel.querySelector("#voiceBtn");
|
|
||||||
const loading = panel.querySelector("#aiLoading");
|
|
||||||
const statusText = panel.querySelector("#statusText");
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
input,
|
btn: panel.querySelector("#voiceBtn"),
|
||||||
btn,
|
input: panel.querySelector("#voiceTextInput"),
|
||||||
setLoading: (isLoading) => {
|
setLoading: (loading) => {
|
||||||
loading.style.display = isLoading ? "inline" : "none";
|
panel.querySelector("#aiLoading").style.display = loading ? "inline" : "none";
|
||||||
statusText.style.display = isLoading ? "none" : "inline";
|
panel.querySelector("#statusText").style.visibility = loading ? "hidden" : "visible";
|
||||||
input.disabled = isLoading;
|
},
|
||||||
if (!isLoading) input.focus();
|
setRecording: (isRecording) => {
|
||||||
|
const dot = panel.querySelector("#recordingIndicator");
|
||||||
|
const text = panel.querySelector("#statusText");
|
||||||
|
if (isRecording) {
|
||||||
|
dot.style.display = "inline-block";
|
||||||
|
text.innerText = "正在聆听...";
|
||||||
|
text.style.color = "#ff4d4f";
|
||||||
|
} else {
|
||||||
|
dot.style.display = "none";
|
||||||
|
text.innerText = "准备就绪";
|
||||||
|
text.style.color = "#999";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
+21
-30
@@ -1,36 +1,27 @@
|
|||||||
// 语音识别
|
export function initVoice(panel, onResultCallback) {
|
||||||
|
|
||||||
export function initVoice(panel, handleCommand) {
|
|
||||||
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||||
const supportSpeech = !!SpeechRecognition;
|
if (!SpeechRecognition) return { supportSpeech: false };
|
||||||
let recognition;
|
|
||||||
|
|
||||||
if (supportSpeech) {
|
const recognition = new SpeechRecognition();
|
||||||
recognition = new SpeechRecognition();
|
recognition.lang = "zh-CN";
|
||||||
recognition.lang = "zh-CN";
|
recognition.continuous = false;
|
||||||
recognition.continuous = false;
|
recognition.interimResults = false;
|
||||||
recognition.interimResults = false;
|
|
||||||
|
|
||||||
recognition.onresult = (event) => {
|
recognition.onresult = (event) => {
|
||||||
const text = event.results[0][0].transcript.trim();
|
const text = event.results[0][0].transcript.trim();
|
||||||
handleCommand(text);
|
if (text) {
|
||||||
};
|
console.log("识别结果:", text);
|
||||||
|
onResultCallback(text);
|
||||||
recognition.onerror = (event) => {
|
|
||||||
console.error("语音识别错误", event.error);
|
|
||||||
};
|
|
||||||
|
|
||||||
panel.querySelector("#voiceBtn").onclick = () => recognition.start();
|
|
||||||
} else {
|
|
||||||
panel.querySelector("#voiceBtn").disabled = true;
|
|
||||||
panel.querySelector("#voiceBtn").innerText = "❌";
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener("keydown", (e) => {
|
|
||||||
if (e.altKey && e.key.toLowerCase() === "v" && supportSpeech && recognition) {
|
|
||||||
recognition.start();
|
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
return supportSpeech;
|
recognition.onerror = (event) => {
|
||||||
|
if (event.error !== 'aborted') console.error("语音错误:", event.error);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
supportSpeech: true,
|
||||||
|
start: () => { try { recognition.start(); } catch(e){} },
|
||||||
|
stop: () => { try { recognition.stop(); } catch(e){} }
|
||||||
|
};
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user