Compare commits

...
Author SHA1 Message Date
Cx330 b2e1860f67 补充描述文档 2026-01-25 21:21:55 +08:00
Cx330 34d658c2b9 初版Demo完善 2026-01-22 13:43:59 +08:00
Cx330 1b8f833789 默认关闭语音播放功能 2026-01-17 20:24:08 +08:00
Cx330 c43b5e5ac1 增加AI文本回复 2026-01-15 22:49:48 +08:00
Cx330 eaf75ac17f 更新提示词逻辑,以XML格式 2026-01-15 21:35:04 +08:00
Cx330 53994476c4 增加AI通过上云码添加仪表的功能 2026-01-14 22:21:02 +08:00
11 changed files with 260 additions and 106 deletions

No files matched your search

+25 -1
View File
@@ -1,3 +1,27 @@
# Agent_For_Supmea
本项目是针对隶属于杭州美仪自动化股份有限公司的仪表云Web平台的一款浏览器插件。此插件支持通过语音/文字输入实现对界面的操作,并且支持接入大语言模型的API。
本项目是针对隶属于杭州美仪自动化股份有限公司的仪表云Web平台的一款浏览器插件。此插件支持通过语音/文字输入实现对界面的操作,并且支持接入大语言模型的API。
## 实现原理
本项目通过LLM模型对接收到的自然语言进行处理,提取出其中的关键词用于控制界面的跳转、文本框输入以及按钮的点击功能。
## 使用操作
- 下载文件压缩包并将其解压。
- 打开浏览器(chrome、edge)在地址栏输入下列地址,,进入到拓展界面。
```c
edge://extensions/
```
- 打开开发人员模式。
- 点击解压缩的拓展,并选择刚刚解压的文件夹。
- 访问仪表云: https://1718cloud.com
- 在浏览器右上角点击拓展图标中的Agent_For_Supmea插件,输入LLM模型的API、Key、Model后点击确认完成操作。
**注**:目前测试API支持**硅基流动**、魔搭(modelscape)、openrouter。
+23 -14
View File
@@ -1,23 +1,18 @@
// 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) => {
// 获取 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. 发起请求
// 向 AI 平台发起 fetch 请求
const response = await fetch(`${config.apiUrl}/chat/completions`, {
method: 'POST',
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();
sendResponse({ success: true, data: data });
// 将 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; // 保持异步
return true;
}
});
+2 -2
View File
@@ -3,7 +3,7 @@ script.type = 'module';
script.src = chrome.runtime.getURL('main.js');
document.head.appendChild(script);
// 监听来自网页(main.js)的请求
// 监听来自 main.js 的请求
window.addEventListener("DO_AI_REQUEST", async (event) => {
const { userInput, systemPrompt } = event.detail;
@@ -13,7 +13,7 @@ window.addEventListener("DO_AI_REQUEST", async (event) => {
userInput,
systemPrompt
}, (response) => {
// 将结果传回给网页(main.js)
// 将结果传回给 main.js
window.dispatchEvent(new CustomEvent("AI_RESULT", { detail: response }));
});
});
+45 -25
View File
@@ -1,4 +1,3 @@
// main.js
import { createPanel } from './scripts/panel.js';
import { handleCommand, COMMANDS } from './scripts/commands.js';
import { initVoice } from './scripts/voice.js';
@@ -9,49 +8,77 @@ const init = async () => {
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) {
if (!text) return;
ui.setLoading(true);
try {
const localMatch = COMMANDS.find(c => text.includes(c.key));
if (localMatch) {
handleCommand(localMatch.key);
const aiResponse = await translateToCommand(text);
if (aiResponse && aiResponse.content) {
handleCommand(aiResponse.content, uiRefs, aiResponse.voiceEnabled);
} else {
const aiResult = await translateToCommand(text);
if (aiResult) handleCommand(aiResult);
uiRefs.updateStatus("未识别到有效指令");
}
} catch (err) {
console.error("处理流程错误:", err);
uiRefs.updateStatus("处理指令时出错");
} finally {
ui.setLoading(false);
}
}
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);
});
// --- 逻辑 A: 保留原有按钮点击录音 ---
// 输入框回车触发
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;
// 按钮模式下 3秒后自动停止,或靠 recognition 自动结束
setTimeout(() => {
if (isRecording) {
voiceCtrl.stop();
ui.setRecording(false);
isRecording = false;
}
}, 3000);
}, 4000);
} else {
voiceCtrl.stop();
ui.setRecording(false);
isRecording = false;
}
};
// --- 逻辑 B: 空格长按 0.5s 触发 ---
// 空格长按逻辑
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();
@@ -69,25 +96,18 @@ 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);
if (document.readyState === "complete" || document.readyState === "interactive") {
init();
} else {
window.addEventListener("DOMContentLoaded", init);
}
+2 -2
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Supmea Automation AI",
"version": "0.1.0",
"name": "Agent_For_Supmea",
"version": "0.1.17",
"permissions": ["activeTab", "storage"],
"background": {
"service_worker": "background/background.js"
+24 -3
View File
@@ -13,15 +13,28 @@
.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; }
</style>
</head>
<body>
<h3>⚙️ AI 模型配置</h3>
<h3> AI 模型配置</h3>
<div class="item">
<label>API Base URL</label>
<input type="text" id="apiUrl" placeholder="https://api-inference.modelscope.cn/v1">
<div class="note">通常使用兼容模式地址</div>
</div>
<div class="item">
@@ -31,12 +44,20 @@
<div class="item">
<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>
<button id="save" class="btn">保存并生效</button>
<div id="status"></div>
<div class="note">提示:保存配置后,请刷新目标页面使设置生效。</div>
<script src="popup.js"></script>
</body>
</html>
+15 -6
View File
@@ -1,12 +1,15 @@
// 页面加载时读取存储的配置
document.addEventListener('DOMContentLoaded', () => {
chrome.storage.sync.get(['aiConfig'], (result) => {
chrome.storage.sync.get(['aiConfig', 'voiceEnabled'], (result) => {
const config = result.aiConfig || {};
// 如果没有存储的值,则显示默认占位符或默认值
document.getElementById('apiUrl').value = config.apiUrl || DEFAULT_URL;
document.getElementById('apiKey').value = config.apiKey || "";
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
};
const isVoiceEnabled = document.getElementById('voiceEnabled').checked;
// 验证 API Key 是否填写
if (!config.apiKey) {
showStatus("❌ 请输入 API Key", "#f56c6c");
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");
});
});
// 状态提示函数
function showStatus(text, color) {
const status = document.getElementById('status');
status.textContent = text;
status.style.color = color;
// 3秒后清除提示
setTimeout(() => {
status.textContent = '';
}, 3000);
+34 -10
View File
@@ -1,28 +1,52 @@
// ai.js
import { COMMANDS } from './commands.js';
export async function translateToCommand(userInput) {
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) => {
// 1. 监听结果
const handler = (event) => {
const response = event.detail;
window.removeEventListener("AI_RESULT", handler);
if (response.success) {
const content = response.data.choices[0].message.content.trim();
console.log("📥 AI 识别结果:", content);
resolve(content === "UNKNOWN" ? null : content);
if (response && response.success && response.data) {
try {
if (response.data.choices && response.data.choices.length > 0) {
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 {
console.error("AI 失败:", response.error);
console.error("AI 请求失败:", response?.error);
resolve(null);
}
};
// 监听来自 content.js 的结果反馈
window.addEventListener("AI_RESULT", handler);
// 2. 触发请求
// 触发自定义事件,由 content.js 转发给 background.js
window.dispatchEvent(new CustomEvent("DO_AI_REQUEST", {
detail: { userInput, systemPrompt }
}));
+88 -39
View File
@@ -39,51 +39,100 @@ export const COMMANDS = [
{ key: "数据下云", menu: "数据下云", route: "/系统管理/数据下云" },
];
export function expandParentMenu(span) {
const subMenu = span.closest(".el-sub-menu");
if (subMenu) {
const title = subMenu.querySelector(".el-sub-menu__title");
if (title && !subMenu.classList.contains("is-opened")) {
title.click();
console.log("📂 已展开父菜单");
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();
}
}
parent = parent.parentElement.closest('li');
}
}
}
export function goToRoute(route) {
// 兼容 Hash 路由和普通路由,根据实际项目情况
window.location.hash = route;
console.log("✅ 已跳转到路由:", route);
function autoFillAndSubmit(arg) {
if (!arg) return;
// 等待页面跳转和组件渲染
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) {
console.log("🚀 执行指令逻辑:", keyOrText);
// 尝试精确匹配 (AI返回的Key) 或 模糊匹配 (用户输入的Text)
const command = COMMANDS.find(c => c.key === keyOrText) ||
COMMANDS.find(c => keyOrText.includes(c.key));
export function handleCommand(aiResult, uiRefs, voiceEnabled = false) {
if (!command) {
console.warn("无法执行,未找到对应菜单动作:", keyOrText);
return;
}
if (typeof aiResult !== 'string' || aiResult === "UNKNOWN") return;
// 查找菜单 DOM 元素 (根据你原有的逻辑)
const allSpans = Array.from(document.querySelectorAll("span"));
// 优先全匹配,防止"报表"匹配到"高级报表"
let span = allSpans.find(el => el.innerText.trim() === command.menu);
// 解析对话内容
const commMatch = aiResult.match(/<communication>([\s\S]*?)<\/communication>/);
if (commMatch && commMatch[1]) {
const speechText = commMatch[1].trim();
if (uiRefs && uiRefs.updateStatus) {
uiRefs.updateStatus(speechText);
}
if (span) {
expandParentMenu(span);
// 模拟点击触发 Vue/React 的路由跳转,或者直接改 Hash
// 如果页面需要点击才能触发加载,建议 span.click()
// 如果只是纯 hash 跳转,goToRoute 即可
// 建议先点击,确保侧边栏高亮
span.click();
goToRoute(command.route);
} else {
console.error(`❌ 未找到菜单元素: ${command.menu}`);
}
// 语音播放判断
if (voiceEnabled) {
const utterance = new SpeechSynthesisUtterance(speechText);
utterance.lang = "zh-CN";
window.speechSynthesis.speak(utterance);
}
}
// 解析指令和参数
const cmdMatch = aiResult.match(/<cmd>([\s\S]*?)<\/cmd>/);
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);
}
}
}
}
-1
View File
@@ -1,4 +1,3 @@
// scripts/panel.js
export function createPanel() {
const panelId = "automation-ai-panel";
let panel = document.getElementById(panelId);
+2 -3
View File
@@ -1,17 +1,16 @@
// scripts/voice.js
export function initVoice(panel, onResultCallback) {
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
if (!SpeechRecognition) return { supportSpeech: false };
const recognition = new SpeechRecognition();
recognition.lang = "zh-CN";
recognition.continuous = false; // 改为 false,确保每次停止都能立即结算
recognition.continuous = false;
recognition.interimResults = false;
recognition.onresult = (event) => {
const text = event.results[0][0].transcript.trim();
if (text) {
console.log("🎤 识别结果:", text);
console.log("识别结果:", text);
onResultCallback(text);
}
};